diff --git a/docs/mechanical-verification/cp9-readonly/calls/0015-fusion_mechanical.json b/docs/mechanical-verification/cp9-readonly/calls/0015-fusion_mechanical.json new file mode 100644 index 0000000..a03d2cf --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0015-fusion_mechanical.json @@ -0,0 +1,93 @@ +{ + "target": "adom_drew2", + "verb": "fusion_mechanical", + "args": { + "op": "assembly_properties", + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + } + }, + "requestJson": "{\"op\": \"assembly_properties\", \"target\": {\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"}}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235867.04024, + "mutating": false, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"area\": 2200.0,\n \"averageDensity\": 2.7e-6,\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"entity\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"objectType\": \"adsk::fusion::Component\",\n \"occurrencePath\": null,\n \"occurrenceToken\": null,\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\n \"rootComponentId\": \"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\n \"token\": \"/v4BAAEAAwAAAAAAAAAAAAAA\"\n },\n \"mass\": 0.0162,\n \"materialBasis\": \"Fusion assigned/default material; supplier provenance does not certify modeled material density\",\n \"scope\": \"component_subtree\",\n \"space\": \"component\",\n \"target\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"\n },\n \"units\": {\n \"area\": \"mm2\",\n \"averageDensity\": \"kg/mm3\",\n \"centerOfMass\": \"mm\",\n \"mass\": \"kg\",\n \"volume\": \"mm3\"\n },\n \"volume\": 6000.0\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 841,\n \"error\": \"\",\n \"operation\": \"assembly_properties\",\n \"output\": \"{\\\"_hint\\\":\\\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\\\",\\\"_hintDocumentHygiene\\\":\\\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\\\",\\\"_timeoutHint\\\":\\\"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\\\",\\\"data\\\":{\\\"area\\\":2200.0,\\\"averageDensity\\\":2.7e-6,\\\"centerOfMass\\\":[10.0,15.0,5.0],\\\"entity\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"objectType\\\":\\\"adsk::fusion::Component\\\",\\\"occurrencePath\\\":null,\\\"occurrenceToken\\\":null,\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\",\\\"rootComponentId\\\":\\\"3400b1c0-7442-4474-9ad7-c5e6343280a7\\\",\\\"token\\\":\\\"/v4BAAEAAwAAAAAAAAAAAAAA\\\"},\\\"mass\\\":0.0162,\\\"materialBasis\\\":\\\"Fusion assigned/default material; supplier provenance does not certify modeled material density\\\",\\\"scope\\\":\\\"component_subtree\\\",\\\"space\\\":\\\"component\\\",\\\"target\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\"},\\\"units\\\":{\\\"area\\\":\\\"mm2\\\",\\\"averageDensity\\\":\\\"kg/mm3\\\",\\\"centerOfMass\\\":\\\"mm\\\",\\\"mass\\\":\\\"kg\\\",\\\"volume\\\":\\\"mm3\\\"},\\\"volume\\\":6000.0},\\\"documentHygiene\\\":{\\\"documents\\\":[{\\\"isModified\\\":false,\\\"isSaved\\\":false,\\\"name\\\":\\\"Untitled\\\",\\\"needsSave\\\":false,\\\"saveStateKnown\\\":true},{\\\"isModified\\\":true,\\\"isSaved\\\":false,\\\"name\\\":\\\"Untitled\\\",\\\"needsSave\\\":true,\\\"saveStateKnown\\\":true}],\\\"inspectionComplete\\\":true,\\\"needsSaveCount\\\":1,\\\"openDocumentCount\\\":2,\\\"ownership\\\":\\\"notInferred\\\",\\\"recoveryHealth\\\":\\\"notInspected\\\"},\\\"operation\\\":\\\"assembly_properties\\\",\\\"sessionId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\",\\\"shell\\\":\\\"rust\\\",\\\"statusVerb\\\":\\\"fusion_get_app_state\\\",\\\"success\\\":true,\\\"timeoutSeconds\\\":60}\",\n \"schemaVersion\": 1,\n \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 60\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235867.8893027, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "area": 2200.0, + "averageDensity": 2.7e-06, + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "entity": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "objectType": "adsk::fusion::Component", + "occurrencePath": null, + "occurrenceToken": null, + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed", + "rootComponentId": "3400b1c0-7442-4474-9ad7-c5e6343280a7", + "token": "/v4BAAEAAwAAAAAAAAAAAAAA" + }, + "mass": 0.0162, + "materialBasis": "Fusion assigned/default material; supplier provenance does not certify modeled material density", + "scope": "component_subtree", + "space": "component", + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + }, + "units": { + "area": "mm2", + "averageDensity": "kg/mm3", + "centerOfMass": "mm", + "mass": "kg", + "volume": "mm3" + }, + "volume": 6000.0 + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 841, + "error": "", + "operation": "assembly_properties", + "output": "{\"_hint\":\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\"_hintDocumentHygiene\":\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\"_timeoutHint\":\"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\"data\":{\"area\":2200.0,\"averageDensity\":2.7e-6,\"centerOfMass\":[10.0,15.0,5.0],\"entity\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"objectType\":\"adsk::fusion::Component\",\"occurrencePath\":null,\"occurrenceToken\":null,\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\"rootComponentId\":\"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\"token\":\"/v4BAAEAAwAAAAAAAAAAAAAA\"},\"mass\":0.0162,\"materialBasis\":\"Fusion assigned/default material; supplier provenance does not certify modeled material density\",\"scope\":\"component_subtree\",\"space\":\"component\",\"target\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"},\"units\":{\"area\":\"mm2\",\"averageDensity\":\"kg/mm3\",\"centerOfMass\":\"mm\",\"mass\":\"kg\",\"volume\":\"mm3\"},\"volume\":6000.0},\"documentHygiene\":{\"documents\":[{\"isModified\":false,\"isSaved\":false,\"name\":\"Untitled\",\"needsSave\":false,\"saveStateKnown\":true},{\"isModified\":true,\"isSaved\":false,\"name\":\"Untitled\",\"needsSave\":true,\"saveStateKnown\":true}],\"inspectionComplete\":true,\"needsSaveCount\":1,\"openDocumentCount\":2,\"ownership\":\"notInferred\",\"recoveryHealth\":\"notInspected\"},\"operation\":\"assembly_properties\",\"sessionId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\"shell\":\"rust\",\"statusVerb\":\"fusion_get_app_state\",\"success\":true,\"timeoutSeconds\":60}", + "schemaVersion": 1, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60 + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0016-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0016-fusion_run_modeling_script.json new file mode 100644 index 0000000..7c732e6 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0016-fusion_run_modeling_script.json @@ -0,0 +1,131 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"action\": \"witness\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"action\\\": \\\"witness\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235867.8958595, + "mutating": false, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"success\": true,\n \"witness\": {\n \"components\": [\n {\n \"attributes\": {\n \"requestedName\": \"Untitled\"\n },\n \"bodies\": [\n {\n \"bounds\": [\n [\n 0.0,\n 0.0,\n 0.0\n ],\n [\n 20.0,\n 30.0,\n 10.0\n ]\n ],\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"faces\": 6,\n \"mass\": 0.0162,\n \"material\": \"Aluminum\",\n \"name\": \"Block\",\n \"volume\": 6000.0\n }\n ],\n \"description\": \"\",\n \"material\": \"Aluminum\",\n \"name\": \"\",\n \"partNumber\": \"2026-10-05-14-31-00-614\",\n \"sketches\": [\n {\n \"curves\": 4,\n \"fullyConstrained\": false,\n \"name\": \"Base rectangle\",\n \"profiles\": 1\n }\n ]\n }\n ],\n \"instanceCount\": 1,\n \"instanceVolume\": 6000.0,\n \"occurrences\": [],\n \"parameters\": {\n \"d5\": \"width\",\n \"d6\": \"height\",\n \"d7\": \"depth\",\n \"d8\": \"0.0 deg\",\n \"depth\": \"10.0 mm\",\n \"height\": \"30.0 mm\",\n \"width\": \"20.0 mm\"\n },\n \"physical\": {\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"mass\": 0.0162,\n \"volume\": 6000.0\n },\n \"timelineCount\": 2\n }\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1768,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235869.6722748, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true, + "witness": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + } + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1768, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0017-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0017-fusion_run_modeling_script.json new file mode 100644 index 0000000..07478d9 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0017-fusion_run_modeling_script.json @@ -0,0 +1,595 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import threading\nif threading.current_thread() is not threading.main_thread():\n raise RuntimeError('Native render reads require the main thread')\nimport builtins, json, platform, re, sys\nREQUEST = json.loads('{\"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\", \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\"}')\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any((d == document for d in app.documents)), 'Exact owned document is no longer open')\n require(not any((d == document for d in run['baselineObjects'])), 'Refusing a baseline document')\n require(document.name == row['name'] and (not document.isSaved), 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\"\"\"Bounded CP9 native read-only probe; import alone makes no native calls.\n\nUse owned_script with the maintained native registry. Availability is not render\nsuccess, native queue idleness, a complete scene witness or settings ownership.\n\"\"\"\nimport inspect\nimport ast\nimport json\nimport math\nfrom pathlib import Path\nimport threading\n\nMAX_ENVIRONMENTS = 32\nMAX_OUTPUT_BYTES = 128 * 1024\n\n\ndef render_capability_probe(app, document, design, *, max_environments=16):\n import adsk.core\n import adsk.fusion\n\n def need(value, message):\n if not value: raise RuntimeError(message)\n\n need(threading.current_thread() is threading.main_thread(), 'Native render reads require the main thread')\n need(type(max_environments) is int and 0 <= max_environments <= MAX_ENVIRONMENTS, 'Invalid environment probe budget')\n need(document is not None and document.isValid and design is not None and design.isValid, 'Owned native design is invalid')\n need(app.activeDocument == document and design.parentDocument == document, 'Current native document/design identity differs')\n document_count = app.documents.count\n need(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget or has invalid count')\n need(sum(app.documents.item(i) == document for i in range(document_count)) == 1, 'Exact owned document is not uniquely open')\n result = {'probeVersion':1, 'status':'DIAGNOSTIC_ONLY', 'renderStarted':False, 'settingsWritten':False,\n 'sceneWitnessComplete':False, 'nativeQueueIdle':'UNVERIFIED', 'exclusiveSettingsOwnership':'UNVERIFIED',\n 'errors':[], 'classes':{}, 'camera':{}, 'rendering':{}, 'scene':{}, 'environments':{},\n 'gaps':['No render execution or RenderFuture instance was created.',\n 'No global renderer queue/coexistence proof.',\n 'Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.',\n 'Environment IDs and names do not prove custom environment content identity.',\n 'No admitted revision-to-pixels or full source snapshot claim.']}\n\n def text(value):\n need(isinstance(value,str) and len(value) <= 256, 'Native string exceeds probe budget or has wrong type')\n return value\n\n def number(value):\n need(type(value) in (int,float) and math.isfinite(value), 'Native scalar is not a finite number')\n return value\n\n def boolean(value):\n need(type(value) is bool, 'Native scalar is not Boolean')\n return value\n\n def enum(value):\n need(not isinstance(value,(bool,str)), 'Native enum has invalid type')\n result = int(value)\n need(result == value, 'Native enum is not an exact integer')\n return result\n\n def point(value, scale=10):\n return [number(number(getattr(value,axis)) * scale) for axis in ('x','y','z')]\n\n def read(path, callback, unit=None, native_unit=None):\n try:\n data = callback()\n row = {'available':True,'value':data}\n if unit is not None: row['unit'] = unit\n if native_unit is not None: row['nativeUnit'] = native_unit\n return row\n except Exception as error:\n reason = str(error)[:512]\n result['errors'].append({'field':path,'error':reason})\n return {'available':False,'error':reason}\n\n def skipped(reason): return {'available':None,'notRead':reason}\n\n def object_read(path, callback):\n try:\n value = callback()\n need(value is not None and value.isValid, 'Native object absent or invalid')\n text(value.objectType)\n return value, {'available':True,'objectType':value.objectType}\n except Exception as error:\n reason = str(error)[:512]\n result['errors'].append({'field':path,'error':reason})\n return None, {'available':False,'error':reason}\n\n def fields(destination, prefix, owner, specs):\n for name, converter, unit, native_unit in specs:\n destination[name] = read(prefix + '.' + name, lambda name=name,converter=converter:converter(getattr(owner,name)), unit, native_unit)\n\n result['documentBefore'] = {'modified':read('document.modified',lambda:boolean(document.isModified)),\n 'saved':read('document.saved',lambda:boolean(document.isSaved))}\n # Static class inspection does not invoke descriptors, constructors, start,\n # activation, cancel or any native future polling.\n class_members = {'RenderManager':('rendering','sceneSettings','renderEnvironments','lights','isRenderWorkspaceActive'),\n 'Rendering':('startLocalRender','aspectRatio','resolution','resolutionWidth','resolutionHeight','renderQuality','isBackgroundTransparent'),\n 'RenderFuture':('filename','imageWidth','imageHeight','progress','renderState','isValid'),\n 'SceneSettings':('backgroundType','backgroundEnvironment','backgroundSolidColor','isDepthOfFieldEnabled'),\n 'RenderEnvironment':('id','name','isCustomEnvironment')}\n for name,members in class_members.items():\n try:\n cls = getattr(adsk.fusion,name)\n availability = {}\n for member in members:\n try: inspect.getattr_static(cls,member); availability[member] = True\n except AttributeError: availability[member] = False\n result['classes'][name] = {'available':True,'members':availability,'methodInvocation':False}\n except Exception as error:\n result['classes'][name] = {'available':False,'error':str(error)[:512]}\n\n camera_enums = {name:read('CameraTypes.'+name,lambda name=name:enum(getattr(adsk.core.CameraTypes,name)))\n for name in ('OrthographicCameraType','PerspectiveCameraType','PerspectiveWithOrthoFacesCameraType')}\n background_enums = {name:read('RenderSceneBackgroundTypes.'+name,\n lambda name=name:enum(getattr(adsk.fusion.RenderSceneBackgroundTypes,name)))\n for name in ('SolidColorRenderSceneBackgroundType','EnvironmentRenderSceneBackgroundType')}\n result['enums'] = {'camera':camera_enums,'background':background_enums}\n\n viewport, result['viewport'] = object_read('activeViewport',lambda:app.activeViewport)\n camera = None\n if viewport is not None:\n camera, result['camera']['object'] = object_read('camera',lambda:viewport.camera)\n if camera is not None:\n fields(result['camera'],'camera',camera,[('cameraType',enum,'native CameraTypes enum',None),\n ('eye',point,'mm','cm'),('target',point,'mm','cm'),('upVector',lambda p:point(p,1),'unitless',None),\n ('isFitView',boolean,None,None),('isSmoothTransition',boolean,None,None)])\n result['camera']['space'] = 'world/root'\n kind = result['camera']['cameraType'].get('value')\n if kind is not None and kind == camera_enums['OrthographicCameraType'].get('value'):\n def extents():\n okay,width,height = camera.getExtents()\n need(okay is True, 'Orthographic camera extent read refused')\n return [number(number(width)*10),number(number(height)*10)]\n result['camera']['extents'] = read('camera.getExtents',extents,'mm','cm')\n result['camera']['perspectiveAngle'] = skipped('Orthographic camera')\n elif kind is not None and kind in (camera_enums['PerspectiveCameraType'].get('value'),camera_enums['PerspectiveWithOrthoFacesCameraType'].get('value')):\n result['camera']['perspectiveAngle'] = read('camera.perspectiveAngle',lambda:number(camera.perspectiveAngle),\n 'native angular scalar; convention requires native qualification')\n result['camera']['extents'] = skipped('Perspective camera')\n else:\n result['errors'].append({'field':'camera.cameraType','error':'Unknown/unreadable camera mode; conditional lens getters not read'})\n result['camera']['extents'] = result['camera']['perspectiveAngle'] = skipped('Unknown camera mode')\n\n manager, result['renderManager'] = object_read('design.renderManager',lambda:design.renderManager)\n if manager is not None:\n matched = read('renderManager.parentDesign',lambda:boolean(manager.parentDesign == design))\n result['renderManager']['ownerMatches'] = matched\n if matched.get('value') is not True:\n result['errors'].append({'field':'renderManager.parentDesign','error':'Foreign/unreadable renderer owner; remaining renderer objects not read'})\n manager = None\n if manager is not None:\n result['renderManager']['workspaceActive'] = read('renderManager.isRenderWorkspaceActive',lambda:boolean(manager.isRenderWorkspaceActive))\n rendering, result['rendering']['object'] = object_read('renderManager.rendering',lambda:manager.rendering)\n if rendering is not None:\n fields(result['rendering'],'rendering',rendering,[('aspectRatio',enum,'native RenderAspectRatios enum',None),\n ('resolution',enum,'native RenderResolutions enum',None),('resolutionWidth',number,'px',None),\n ('resolutionHeight',number,'px',None),('renderQuality',number,'native quality (25..100)',None),\n ('isBackgroundTransparent',boolean,None,None)])\n scene, result['scene']['object'] = object_read('renderManager.sceneSettings',lambda:manager.sceneSettings)\n if scene is not None:\n specs = [('aspectRatio',enum,'native RenderAspectRatios enum',None),('aspectRatioWidth',number,'ratio',None),\n ('aspectRatioHeight',number,'ratio',None),('backgroundType',enum,'native RenderSceneBackgroundTypes enum',None),\n ('brightness',number,'lux',None),('cameraExposure',number,'EV',None),('cameraFocalLength',number,'mm','mm'),\n ('cameraType',enum,'native CameraTypes enum',None),('groundOffset',lambda n:number(number(n)*10),'mm','cm'),\n ('lightAngle',lambda n:number(math.degrees(number(n))),'deg','rad')]\n specs += [(name,boolean,None,None) for name in ('isDepthOfFieldEnabled','isGroundDisplayed','isGroundFlattened','isGroundReflections')]\n fields(result['scene'],'scene',scene,specs)\n for condition,names in (('isDepthOfFieldEnabled',('centerOfFocus','depthOfFieldBlur')),\n ('isGroundFlattened',('groundPosition',)),('isGroundReflections',('groundRoughness',))):\n enabled = result['scene'][condition].get('value')\n for name in names:\n if enabled is True:\n vector = name in ('centerOfFocus','groundPosition')\n fields(result['scene'],'scene',scene,[(name,point if vector else number,'mm' if vector else 'unitless','cm' if vector else None)])\n else: result['scene'][name] = skipped(condition + ' is false or unreadable')\n background = result['scene']['backgroundType'].get('value')\n if background is not None and background == background_enums['SolidColorRenderSceneBackgroundType'].get('value'):\n def color():\n value = scene.backgroundSolidColor\n return {name:number(getattr(value,name)) for name in ('red','green','blue','opacity')}\n result['scene']['backgroundSolidColor'] = read('scene.backgroundSolidColor',color,'RGBA 0..255; opacity ignored by scene')\n result['scene']['backgroundEnvironment'] = skipped('Solid background')\n elif background is not None and background == background_enums['EnvironmentRenderSceneBackgroundType'].get('value'):\n env, descriptor = object_read('scene.backgroundEnvironment',lambda:scene.backgroundEnvironment)\n if env is not None:\n fields(descriptor,'scene.backgroundEnvironment',env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)])\n result['scene']['backgroundEnvironment'] = descriptor\n result['scene']['backgroundSolidColor'] = skipped('Environment background')\n else:\n result['errors'].append({'field':'scene.backgroundType','error':'Unknown/unreadable background mode; conditional getters not read'})\n result['scene']['backgroundEnvironment'] = result['scene']['backgroundSolidColor'] = skipped('Unknown background mode')\n environments, result['environments']['object'] = object_read('renderManager.renderEnvironments',lambda:manager.renderEnvironments)\n if environments is not None:\n count = read('environments.count',lambda:number(environments.count))\n result['environments']['count'] = count\n n = count.get('value');result['environments']['items'] = []\n if type(n) is int and n >= 0:\n result['environments']['truncated'] = n > max_environments\n for index in range(min(n,max_environments)):\n env, descriptor = object_read('environment.' + str(index),lambda index=index:environments.item(index))\n if env is not None:\n fields(descriptor,'environment.'+str(index),env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)])\n result['environments']['items'].append(descriptor)\n else: result['errors'].append({'field':'environments.count','error':'Environment count is not a nonnegative integer'})\n result['documentAfter'] = {'modified':read('document.modified.after',lambda:boolean(document.isModified)),\n 'saved':read('document.saved.after',lambda:boolean(document.isSaved)),\n 'stillActive':read('document.active.after',lambda:boolean(app.activeDocument == document))}\n result['documentFlagsUnchanged'] = (result['documentBefore']['modified'].get('available') is True\n and result['documentBefore']['saved'].get('available') is True\n and result['documentBefore']['modified'] == result['documentAfter']['modified']\n and result['documentBefore']['saved'] == result['documentAfter']['saved']\n and result['documentAfter']['stillActive'].get('value') is True)\n classes_complete = all(row.get('available') is True and all(row.get('members',{}).values()) for row in result['classes'].values())\n result['observationComplete'] = not result['errors'] and classes_complete and not result['environments'].get('truncated',False)\n need(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES,'Render probe output exceeds byte budget')\n return result\n\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n,m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m,'SESSION_ID',None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable')\nregistries = getattr(builtins,'_adom_mechanical_ralph_runs',None)\nrequire(isinstance(registries,dict), 'Native ownership registry unavailable')\nrun = registries.get(REQUEST['runId'])\nrequire(isinstance(run,dict) and run.get('sessionId') == REQUEST['sessionId'], 'Native registry/session missing')\nrequire(isinstance(run.get('documents'),dict), 'Native ownership documents unavailable')\nrequire(isinstance(run.get('baselineObjects'),list) and len(run['baselineObjects']) <= 128,\n 'Baseline object inventory exceeds probe budget or is unavailable')\ndocument_count = app.documents.count\nrequire(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget')\nrow = owned(REQUEST['documentId'])\ndocument = row['object']\nrequire(app.activeDocument == document, 'Owned render probe document must already be active')\ndesign = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\nresult = {'success':True, 'documentId':REQUEST['documentId'], 'ownerRunId':REQUEST['runId'],\n 'sessionId':REQUEST['sessionId'], 'acceptance':'DIAGNOSTIC_ONLY',\n 'renderProbe':render_capability_probe(app,document,design)}\nrequire(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES,\n 'Whole render diagnostic output exceeds byte budget')\n" + }, + "requestJson": "{\"script\": \"import threading\\nif threading.current_thread() is not threading.main_thread():\\n raise RuntimeError('Native render reads require the main thread')\\nimport builtins, json, platform, re, sys\\nREQUEST = json.loads('{\\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\", \\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\"}')\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any((d == document for d in app.documents)), 'Exact owned document is no longer open')\\n require(not any((d == document for d in run['baselineObjects'])), 'Refusing a baseline document')\\n require(document.name == row['name'] and (not document.isSaved), 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\\"\\\"\\\"Bounded CP9 native read-only probe; import alone makes no native calls.\\n\\nUse owned_script with the maintained native registry. Availability is not render\\nsuccess, native queue idleness, a complete scene witness or settings ownership.\\n\\\"\\\"\\\"\\nimport inspect\\nimport ast\\nimport json\\nimport math\\nfrom pathlib import Path\\nimport threading\\n\\nMAX_ENVIRONMENTS = 32\\nMAX_OUTPUT_BYTES = 128 * 1024\\n\\n\\ndef render_capability_probe(app, document, design, *, max_environments=16):\\n import adsk.core\\n import adsk.fusion\\n\\n def need(value, message):\\n if not value: raise RuntimeError(message)\\n\\n need(threading.current_thread() is threading.main_thread(), 'Native render reads require the main thread')\\n need(type(max_environments) is int and 0 <= max_environments <= MAX_ENVIRONMENTS, 'Invalid environment probe budget')\\n need(document is not None and document.isValid and design is not None and design.isValid, 'Owned native design is invalid')\\n need(app.activeDocument == document and design.parentDocument == document, 'Current native document/design identity differs')\\n document_count = app.documents.count\\n need(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget or has invalid count')\\n need(sum(app.documents.item(i) == document for i in range(document_count)) == 1, 'Exact owned document is not uniquely open')\\n result = {'probeVersion':1, 'status':'DIAGNOSTIC_ONLY', 'renderStarted':False, 'settingsWritten':False,\\n 'sceneWitnessComplete':False, 'nativeQueueIdle':'UNVERIFIED', 'exclusiveSettingsOwnership':'UNVERIFIED',\\n 'errors':[], 'classes':{}, 'camera':{}, 'rendering':{}, 'scene':{}, 'environments':{},\\n 'gaps':['No render execution or RenderFuture instance was created.',\\n 'No global renderer queue/coexistence proof.',\\n 'Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.',\\n 'Environment IDs and names do not prove custom environment content identity.',\\n 'No admitted revision-to-pixels or full source snapshot claim.']}\\n\\n def text(value):\\n need(isinstance(value,str) and len(value) <= 256, 'Native string exceeds probe budget or has wrong type')\\n return value\\n\\n def number(value):\\n need(type(value) in (int,float) and math.isfinite(value), 'Native scalar is not a finite number')\\n return value\\n\\n def boolean(value):\\n need(type(value) is bool, 'Native scalar is not Boolean')\\n return value\\n\\n def enum(value):\\n need(not isinstance(value,(bool,str)), 'Native enum has invalid type')\\n result = int(value)\\n need(result == value, 'Native enum is not an exact integer')\\n return result\\n\\n def point(value, scale=10):\\n return [number(number(getattr(value,axis)) * scale) for axis in ('x','y','z')]\\n\\n def read(path, callback, unit=None, native_unit=None):\\n try:\\n data = callback()\\n row = {'available':True,'value':data}\\n if unit is not None: row['unit'] = unit\\n if native_unit is not None: row['nativeUnit'] = native_unit\\n return row\\n except Exception as error:\\n reason = str(error)[:512]\\n result['errors'].append({'field':path,'error':reason})\\n return {'available':False,'error':reason}\\n\\n def skipped(reason): return {'available':None,'notRead':reason}\\n\\n def object_read(path, callback):\\n try:\\n value = callback()\\n need(value is not None and value.isValid, 'Native object absent or invalid')\\n text(value.objectType)\\n return value, {'available':True,'objectType':value.objectType}\\n except Exception as error:\\n reason = str(error)[:512]\\n result['errors'].append({'field':path,'error':reason})\\n return None, {'available':False,'error':reason}\\n\\n def fields(destination, prefix, owner, specs):\\n for name, converter, unit, native_unit in specs:\\n destination[name] = read(prefix + '.' + name, lambda name=name,converter=converter:converter(getattr(owner,name)), unit, native_unit)\\n\\n result['documentBefore'] = {'modified':read('document.modified',lambda:boolean(document.isModified)),\\n 'saved':read('document.saved',lambda:boolean(document.isSaved))}\\n # Static class inspection does not invoke descriptors, constructors, start,\\n # activation, cancel or any native future polling.\\n class_members = {'RenderManager':('rendering','sceneSettings','renderEnvironments','lights','isRenderWorkspaceActive'),\\n 'Rendering':('startLocalRender','aspectRatio','resolution','resolutionWidth','resolutionHeight','renderQuality','isBackgroundTransparent'),\\n 'RenderFuture':('filename','imageWidth','imageHeight','progress','renderState','isValid'),\\n 'SceneSettings':('backgroundType','backgroundEnvironment','backgroundSolidColor','isDepthOfFieldEnabled'),\\n 'RenderEnvironment':('id','name','isCustomEnvironment')}\\n for name,members in class_members.items():\\n try:\\n cls = getattr(adsk.fusion,name)\\n availability = {}\\n for member in members:\\n try: inspect.getattr_static(cls,member); availability[member] = True\\n except AttributeError: availability[member] = False\\n result['classes'][name] = {'available':True,'members':availability,'methodInvocation':False}\\n except Exception as error:\\n result['classes'][name] = {'available':False,'error':str(error)[:512]}\\n\\n camera_enums = {name:read('CameraTypes.'+name,lambda name=name:enum(getattr(adsk.core.CameraTypes,name)))\\n for name in ('OrthographicCameraType','PerspectiveCameraType','PerspectiveWithOrthoFacesCameraType')}\\n background_enums = {name:read('RenderSceneBackgroundTypes.'+name,\\n lambda name=name:enum(getattr(adsk.fusion.RenderSceneBackgroundTypes,name)))\\n for name in ('SolidColorRenderSceneBackgroundType','EnvironmentRenderSceneBackgroundType')}\\n result['enums'] = {'camera':camera_enums,'background':background_enums}\\n\\n viewport, result['viewport'] = object_read('activeViewport',lambda:app.activeViewport)\\n camera = None\\n if viewport is not None:\\n camera, result['camera']['object'] = object_read('camera',lambda:viewport.camera)\\n if camera is not None:\\n fields(result['camera'],'camera',camera,[('cameraType',enum,'native CameraTypes enum',None),\\n ('eye',point,'mm','cm'),('target',point,'mm','cm'),('upVector',lambda p:point(p,1),'unitless',None),\\n ('isFitView',boolean,None,None),('isSmoothTransition',boolean,None,None)])\\n result['camera']['space'] = 'world/root'\\n kind = result['camera']['cameraType'].get('value')\\n if kind is not None and kind == camera_enums['OrthographicCameraType'].get('value'):\\n def extents():\\n okay,width,height = camera.getExtents()\\n need(okay is True, 'Orthographic camera extent read refused')\\n return [number(number(width)*10),number(number(height)*10)]\\n result['camera']['extents'] = read('camera.getExtents',extents,'mm','cm')\\n result['camera']['perspectiveAngle'] = skipped('Orthographic camera')\\n elif kind is not None and kind in (camera_enums['PerspectiveCameraType'].get('value'),camera_enums['PerspectiveWithOrthoFacesCameraType'].get('value')):\\n result['camera']['perspectiveAngle'] = read('camera.perspectiveAngle',lambda:number(camera.perspectiveAngle),\\n 'native angular scalar; convention requires native qualification')\\n result['camera']['extents'] = skipped('Perspective camera')\\n else:\\n result['errors'].append({'field':'camera.cameraType','error':'Unknown/unreadable camera mode; conditional lens getters not read'})\\n result['camera']['extents'] = result['camera']['perspectiveAngle'] = skipped('Unknown camera mode')\\n\\n manager, result['renderManager'] = object_read('design.renderManager',lambda:design.renderManager)\\n if manager is not None:\\n matched = read('renderManager.parentDesign',lambda:boolean(manager.parentDesign == design))\\n result['renderManager']['ownerMatches'] = matched\\n if matched.get('value') is not True:\\n result['errors'].append({'field':'renderManager.parentDesign','error':'Foreign/unreadable renderer owner; remaining renderer objects not read'})\\n manager = None\\n if manager is not None:\\n result['renderManager']['workspaceActive'] = read('renderManager.isRenderWorkspaceActive',lambda:boolean(manager.isRenderWorkspaceActive))\\n rendering, result['rendering']['object'] = object_read('renderManager.rendering',lambda:manager.rendering)\\n if rendering is not None:\\n fields(result['rendering'],'rendering',rendering,[('aspectRatio',enum,'native RenderAspectRatios enum',None),\\n ('resolution',enum,'native RenderResolutions enum',None),('resolutionWidth',number,'px',None),\\n ('resolutionHeight',number,'px',None),('renderQuality',number,'native quality (25..100)',None),\\n ('isBackgroundTransparent',boolean,None,None)])\\n scene, result['scene']['object'] = object_read('renderManager.sceneSettings',lambda:manager.sceneSettings)\\n if scene is not None:\\n specs = [('aspectRatio',enum,'native RenderAspectRatios enum',None),('aspectRatioWidth',number,'ratio',None),\\n ('aspectRatioHeight',number,'ratio',None),('backgroundType',enum,'native RenderSceneBackgroundTypes enum',None),\\n ('brightness',number,'lux',None),('cameraExposure',number,'EV',None),('cameraFocalLength',number,'mm','mm'),\\n ('cameraType',enum,'native CameraTypes enum',None),('groundOffset',lambda n:number(number(n)*10),'mm','cm'),\\n ('lightAngle',lambda n:number(math.degrees(number(n))),'deg','rad')]\\n specs += [(name,boolean,None,None) for name in ('isDepthOfFieldEnabled','isGroundDisplayed','isGroundFlattened','isGroundReflections')]\\n fields(result['scene'],'scene',scene,specs)\\n for condition,names in (('isDepthOfFieldEnabled',('centerOfFocus','depthOfFieldBlur')),\\n ('isGroundFlattened',('groundPosition',)),('isGroundReflections',('groundRoughness',))):\\n enabled = result['scene'][condition].get('value')\\n for name in names:\\n if enabled is True:\\n vector = name in ('centerOfFocus','groundPosition')\\n fields(result['scene'],'scene',scene,[(name,point if vector else number,'mm' if vector else 'unitless','cm' if vector else None)])\\n else: result['scene'][name] = skipped(condition + ' is false or unreadable')\\n background = result['scene']['backgroundType'].get('value')\\n if background is not None and background == background_enums['SolidColorRenderSceneBackgroundType'].get('value'):\\n def color():\\n value = scene.backgroundSolidColor\\n return {name:number(getattr(value,name)) for name in ('red','green','blue','opacity')}\\n result['scene']['backgroundSolidColor'] = read('scene.backgroundSolidColor',color,'RGBA 0..255; opacity ignored by scene')\\n result['scene']['backgroundEnvironment'] = skipped('Solid background')\\n elif background is not None and background == background_enums['EnvironmentRenderSceneBackgroundType'].get('value'):\\n env, descriptor = object_read('scene.backgroundEnvironment',lambda:scene.backgroundEnvironment)\\n if env is not None:\\n fields(descriptor,'scene.backgroundEnvironment',env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)])\\n result['scene']['backgroundEnvironment'] = descriptor\\n result['scene']['backgroundSolidColor'] = skipped('Environment background')\\n else:\\n result['errors'].append({'field':'scene.backgroundType','error':'Unknown/unreadable background mode; conditional getters not read'})\\n result['scene']['backgroundEnvironment'] = result['scene']['backgroundSolidColor'] = skipped('Unknown background mode')\\n environments, result['environments']['object'] = object_read('renderManager.renderEnvironments',lambda:manager.renderEnvironments)\\n if environments is not None:\\n count = read('environments.count',lambda:number(environments.count))\\n result['environments']['count'] = count\\n n = count.get('value');result['environments']['items'] = []\\n if type(n) is int and n >= 0:\\n result['environments']['truncated'] = n > max_environments\\n for index in range(min(n,max_environments)):\\n env, descriptor = object_read('environment.' + str(index),lambda index=index:environments.item(index))\\n if env is not None:\\n fields(descriptor,'environment.'+str(index),env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)])\\n result['environments']['items'].append(descriptor)\\n else: result['errors'].append({'field':'environments.count','error':'Environment count is not a nonnegative integer'})\\n result['documentAfter'] = {'modified':read('document.modified.after',lambda:boolean(document.isModified)),\\n 'saved':read('document.saved.after',lambda:boolean(document.isSaved)),\\n 'stillActive':read('document.active.after',lambda:boolean(app.activeDocument == document))}\\n result['documentFlagsUnchanged'] = (result['documentBefore']['modified'].get('available') is True\\n and result['documentBefore']['saved'].get('available') is True\\n and result['documentBefore']['modified'] == result['documentAfter']['modified']\\n and result['documentBefore']['saved'] == result['documentAfter']['saved']\\n and result['documentAfter']['stillActive'].get('value') is True)\\n classes_complete = all(row.get('available') is True and all(row.get('members',{}).values()) for row in result['classes'].values())\\n result['observationComplete'] = not result['errors'] and classes_complete and not result['environments'].get('truncated',False)\\n need(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES,'Render probe output exceeds byte budget')\\n return result\\n\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n,m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m,'SESSION_ID',None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable')\\nregistries = getattr(builtins,'_adom_mechanical_ralph_runs',None)\\nrequire(isinstance(registries,dict), 'Native ownership registry unavailable')\\nrun = registries.get(REQUEST['runId'])\\nrequire(isinstance(run,dict) and run.get('sessionId') == REQUEST['sessionId'], 'Native registry/session missing')\\nrequire(isinstance(run.get('documents'),dict), 'Native ownership documents unavailable')\\nrequire(isinstance(run.get('baselineObjects'),list) and len(run['baselineObjects']) <= 128,\\n 'Baseline object inventory exceeds probe budget or is unavailable')\\ndocument_count = app.documents.count\\nrequire(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget')\\nrow = owned(REQUEST['documentId'])\\ndocument = row['object']\\nrequire(app.activeDocument == document, 'Owned render probe document must already be active')\\ndesign = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\nresult = {'success':True, 'documentId':REQUEST['documentId'], 'ownerRunId':REQUEST['runId'],\\n 'sessionId':REQUEST['sessionId'], 'acceptance':'DIAGNOSTIC_ONLY',\\n 'renderProbe':render_capability_probe(app,document,design)}\\nrequire(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES,\\n 'Whole render diagnostic output exceeds byte budget')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235869.6814604, + "mutating": true, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"acceptance\": \"DIAGNOSTIC_ONLY\",\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"ownerRunId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\",\n \"renderProbe\": {\n \"camera\": {\n \"object\": {\n \"available\": false,\n \"error\": \"Native object absent or invalid\"\n }\n },\n \"classes\": {\n \"RenderEnvironment\": {\n \"available\": true,\n \"members\": {\n \"id\": true,\n \"isCustomEnvironment\": true,\n \"name\": true\n },\n \"methodInvocation\": false\n },\n \"RenderFuture\": {\n \"available\": true,\n \"members\": {\n \"filename\": true,\n \"imageHeight\": true,\n \"imageWidth\": true,\n \"isValid\": true,\n \"progress\": true,\n \"renderState\": true\n },\n \"methodInvocation\": false\n },\n \"RenderManager\": {\n \"available\": true,\n \"members\": {\n \"isRenderWorkspaceActive\": true,\n \"lights\": true,\n \"renderEnvironments\": true,\n \"rendering\": true,\n \"sceneSettings\": true\n },\n \"methodInvocation\": false\n },\n \"Rendering\": {\n \"available\": true,\n \"members\": {\n \"aspectRatio\": true,\n \"isBackgroundTransparent\": true,\n \"renderQuality\": true,\n \"resolution\": true,\n \"resolutionHeight\": true,\n \"resolutionWidth\": true,\n \"startLocalRender\": true\n },\n \"methodInvocation\": false\n },\n \"SceneSettings\": {\n \"available\": true,\n \"members\": {\n \"backgroundEnvironment\": true,\n \"backgroundSolidColor\": true,\n \"backgroundType\": true,\n \"isDepthOfFieldEnabled\": true\n },\n \"methodInvocation\": false\n }\n },\n \"documentAfter\": {\n \"modified\": {\n \"available\": true,\n \"value\": true\n },\n \"saved\": {\n \"available\": true,\n \"value\": false\n },\n \"stillActive\": {\n \"available\": true,\n \"value\": true\n }\n },\n \"documentBefore\": {\n \"modified\": {\n \"available\": true,\n \"value\": true\n },\n \"saved\": {\n \"available\": true,\n \"value\": false\n }\n },\n \"documentFlagsUnchanged\": true,\n \"enums\": {\n \"background\": {\n \"EnvironmentRenderSceneBackgroundType\": {\n \"available\": true,\n \"value\": 0\n },\n \"SolidColorRenderSceneBackgroundType\": {\n \"available\": true,\n \"value\": 1\n }\n },\n \"camera\": {\n \"OrthographicCameraType\": {\n \"available\": true,\n \"value\": 0\n },\n \"PerspectiveCameraType\": {\n \"available\": true,\n \"value\": 1\n },\n \"PerspectiveWithOrthoFacesCameraType\": {\n \"available\": true,\n \"value\": 2\n }\n }\n },\n \"environments\": {\n \"count\": {\n \"available\": true,\n \"value\": 14\n },\n \"items\": [\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"Studio1\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Sharp Highlights\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"Studio2\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Rim Highlights\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"Studio3\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Cool Light\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"Studio5\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Warm Light\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"Studio6\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Soft Light\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"Studio7\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Grid Light\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"CustomLoadedEnvironment\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": true\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Custom...\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"crossroads\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Crossroads\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"DryLakeBed\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Dry lake bed\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"field\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Field\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"photobooth\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Photobooth\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"mi360_Plaza\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Plaza\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"skylight\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Skylight\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n },\n {\n \"available\": true,\n \"id\": {\n \"available\": true,\n \"value\": \"mi360_Snow\"\n },\n \"isCustomEnvironment\": {\n \"available\": true,\n \"value\": false\n },\n \"name\": {\n \"available\": true,\n \"value\": \"Snow Field\"\n },\n \"objectType\": \"adsk::fusion::RenderEnvironment\"\n }\n ],\n \"object\": {\n \"available\": true,\n \"objectType\": \"adsk::fusion::RenderEnvironments\"\n },\n \"truncated\": false\n },\n \"errors\": [\n {\n \"error\": \"Native object absent or invalid\",\n \"field\": \"camera\"\n }\n ],\n \"exclusiveSettingsOwnership\": \"UNVERIFIED\",\n \"gaps\": [\n \"No render execution or RenderFuture instance was created.\",\n \"No global renderer queue/coexistence proof.\",\n \"Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.\",\n \"Environment IDs and names do not prove custom environment content identity.\",\n \"No admitted revision-to-pixels or full source snapshot claim.\"\n ],\n \"nativeQueueIdle\": \"UNVERIFIED\",\n \"observationComplete\": false,\n \"probeVersion\": 1,\n \"renderManager\": {\n \"available\": true,\n \"objectType\": \"adsk::fusion::RenderManager\",\n \"ownerMatches\": {\n \"available\": true,\n \"value\": true\n },\n \"workspaceActive\": {\n \"available\": true,\n \"value\": false\n }\n },\n \"renderStarted\": false,\n \"rendering\": {\n \"aspectRatio\": {\n \"available\": true,\n \"unit\": \"native RenderAspectRatios enum\",\n \"value\": 0\n },\n \"isBackgroundTransparent\": {\n \"available\": true,\n \"value\": false\n },\n \"object\": {\n \"available\": true,\n \"objectType\": \"adsk::fusion::Rendering\"\n },\n \"renderQuality\": {\n \"available\": true,\n \"unit\": \"native quality (25..100)\",\n \"value\": 75\n },\n \"resolution\": {\n \"available\": true,\n \"unit\": \"native RenderResolutions enum\",\n \"value\": 17\n },\n \"resolutionHeight\": {\n \"available\": true,\n \"unit\": \"px\",\n \"value\": 1242\n },\n \"resolutionWidth\": {\n \"available\": true,\n \"unit\": \"px\",\n \"value\": 1961\n }\n },\n \"scene\": {\n \"aspectRatio\": {\n \"available\": true,\n \"unit\": \"native RenderAspectRatios enum\",\n \"value\": 0\n },\n \"aspectRatioHeight\": {\n \"available\": true,\n \"unit\": \"ratio\",\n \"value\": 0\n },\n \"aspectRatioWidth\": {\n \"available\": true,\n \"unit\": \"ratio\",\n \"value\": 0\n },\n \"backgroundEnvironment\": {\n \"available\": null,\n \"notRead\": \"Solid background\"\n },\n \"backgroundSolidColor\": {\n \"available\": true,\n \"unit\": \"RGBA 0..255; opacity ignored by scene\",\n \"value\": {\n \"blue\": 178,\n \"green\": 178,\n \"opacity\": 255,\n \"red\": 178\n }\n },\n \"backgroundType\": {\n \"available\": true,\n \"unit\": \"native RenderSceneBackgroundTypes enum\",\n \"value\": 1\n },\n \"brightness\": {\n \"available\": true,\n \"unit\": \"lux\",\n \"value\": 2000.0\n },\n \"cameraExposure\": {\n \"available\": true,\n \"unit\": \"EV\",\n \"value\": 12.0\n },\n \"cameraFocalLength\": {\n \"available\": true,\n \"nativeUnit\": \"mm\",\n \"unit\": \"mm\",\n \"value\": 90.0\n },\n \"cameraType\": {\n \"available\": true,\n \"unit\": \"native CameraTypes enum\",\n \"value\": 0\n },\n \"centerOfFocus\": {\n \"available\": null,\n \"notRead\": \"isDepthOfFieldEnabled is false or unreadable\"\n },\n \"depthOfFieldBlur\": {\n \"available\": null,\n \"notRead\": \"isDepthOfFieldEnabled is false or unreadable\"\n },\n \"groundOffset\": {\n \"available\": true,\n \"nativeUnit\": \"cm\",\n \"unit\": \"mm\",\n \"value\": 0.0\n },\n \"groundPosition\": {\n \"available\": null,\n \"notRead\": \"isGroundFlattened is false or unreadable\"\n },\n \"groundRoughness\": {\n \"available\": null,\n \"notRead\": \"isGroundReflections is false or unreadable\"\n },\n \"isDepthOfFieldEnabled\": {\n \"available\": true,\n \"value\": false\n },\n \"isGroundDisplayed\": {\n \"available\": true,\n \"value\": true\n },\n \"isGroundFlattened\": {\n \"available\": true,\n \"value\": false\n },\n \"isGroundReflections\": {\n \"available\": true,\n \"value\": false\n },\n \"lightAngle\": {\n \"available\": true,\n \"nativeUnit\": \"rad\",\n \"unit\": \"deg\",\n \"value\": -0.0\n },\n \"object\": {\n \"available\": true,\n \"objectType\": \"adsk::fusion::SceneSettings\"\n }\n },\n \"sceneWitnessComplete\": false,\n \"settingsWritten\": false,\n \"status\": \"DIAGNOSTIC_ONLY\",\n \"viewport\": {\n \"available\": true,\n \"objectType\": \"adsk::core::Viewport\"\n }\n },\n \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1778,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235871.468298, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "acceptance": "DIAGNOSTIC_ONLY", + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "ownerRunId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "renderProbe": { + "camera": { + "object": { + "available": false, + "error": "Native object absent or invalid" + } + }, + "classes": { + "RenderEnvironment": { + "available": true, + "members": { + "id": true, + "isCustomEnvironment": true, + "name": true + }, + "methodInvocation": false + }, + "RenderFuture": { + "available": true, + "members": { + "filename": true, + "imageHeight": true, + "imageWidth": true, + "isValid": true, + "progress": true, + "renderState": true + }, + "methodInvocation": false + }, + "RenderManager": { + "available": true, + "members": { + "isRenderWorkspaceActive": true, + "lights": true, + "renderEnvironments": true, + "rendering": true, + "sceneSettings": true + }, + "methodInvocation": false + }, + "Rendering": { + "available": true, + "members": { + "aspectRatio": true, + "isBackgroundTransparent": true, + "renderQuality": true, + "resolution": true, + "resolutionHeight": true, + "resolutionWidth": true, + "startLocalRender": true + }, + "methodInvocation": false + }, + "SceneSettings": { + "available": true, + "members": { + "backgroundEnvironment": true, + "backgroundSolidColor": true, + "backgroundType": true, + "isDepthOfFieldEnabled": true + }, + "methodInvocation": false + } + }, + "documentAfter": { + "modified": { + "available": true, + "value": true + }, + "saved": { + "available": true, + "value": false + }, + "stillActive": { + "available": true, + "value": true + } + }, + "documentBefore": { + "modified": { + "available": true, + "value": true + }, + "saved": { + "available": true, + "value": false + } + }, + "documentFlagsUnchanged": true, + "enums": { + "background": { + "EnvironmentRenderSceneBackgroundType": { + "available": true, + "value": 0 + }, + "SolidColorRenderSceneBackgroundType": { + "available": true, + "value": 1 + } + }, + "camera": { + "OrthographicCameraType": { + "available": true, + "value": 0 + }, + "PerspectiveCameraType": { + "available": true, + "value": 1 + }, + "PerspectiveWithOrthoFacesCameraType": { + "available": true, + "value": 2 + } + } + }, + "environments": { + "count": { + "available": true, + "value": 14 + }, + "items": [ + { + "available": true, + "id": { + "available": true, + "value": "Studio1" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Sharp Highlights" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio2" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Rim Highlights" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio3" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Cool Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio5" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Warm Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio6" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Soft Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio7" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Grid Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "CustomLoadedEnvironment" + }, + "isCustomEnvironment": { + "available": true, + "value": true + }, + "name": { + "available": true, + "value": "Custom..." + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "crossroads" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Crossroads" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "DryLakeBed" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Dry lake bed" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "field" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Field" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "photobooth" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Photobooth" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "mi360_Plaza" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Plaza" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "skylight" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Skylight" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "mi360_Snow" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Snow Field" + }, + "objectType": "adsk::fusion::RenderEnvironment" + } + ], + "object": { + "available": true, + "objectType": "adsk::fusion::RenderEnvironments" + }, + "truncated": false + }, + "errors": [ + { + "error": "Native object absent or invalid", + "field": "camera" + } + ], + "exclusiveSettingsOwnership": "UNVERIFIED", + "gaps": [ + "No render execution or RenderFuture instance was created.", + "No global renderer queue/coexistence proof.", + "Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.", + "Environment IDs and names do not prove custom environment content identity.", + "No admitted revision-to-pixels or full source snapshot claim." + ], + "nativeQueueIdle": "UNVERIFIED", + "observationComplete": false, + "probeVersion": 1, + "renderManager": { + "available": true, + "objectType": "adsk::fusion::RenderManager", + "ownerMatches": { + "available": true, + "value": true + }, + "workspaceActive": { + "available": true, + "value": false + } + }, + "renderStarted": false, + "rendering": { + "aspectRatio": { + "available": true, + "unit": "native RenderAspectRatios enum", + "value": 0 + }, + "isBackgroundTransparent": { + "available": true, + "value": false + }, + "object": { + "available": true, + "objectType": "adsk::fusion::Rendering" + }, + "renderQuality": { + "available": true, + "unit": "native quality (25..100)", + "value": 75 + }, + "resolution": { + "available": true, + "unit": "native RenderResolutions enum", + "value": 17 + }, + "resolutionHeight": { + "available": true, + "unit": "px", + "value": 1242 + }, + "resolutionWidth": { + "available": true, + "unit": "px", + "value": 1961 + } + }, + "scene": { + "aspectRatio": { + "available": true, + "unit": "native RenderAspectRatios enum", + "value": 0 + }, + "aspectRatioHeight": { + "available": true, + "unit": "ratio", + "value": 0 + }, + "aspectRatioWidth": { + "available": true, + "unit": "ratio", + "value": 0 + }, + "backgroundEnvironment": { + "available": null, + "notRead": "Solid background" + }, + "backgroundSolidColor": { + "available": true, + "unit": "RGBA 0..255; opacity ignored by scene", + "value": { + "blue": 178, + "green": 178, + "opacity": 255, + "red": 178 + } + }, + "backgroundType": { + "available": true, + "unit": "native RenderSceneBackgroundTypes enum", + "value": 1 + }, + "brightness": { + "available": true, + "unit": "lux", + "value": 2000.0 + }, + "cameraExposure": { + "available": true, + "unit": "EV", + "value": 12.0 + }, + "cameraFocalLength": { + "available": true, + "nativeUnit": "mm", + "unit": "mm", + "value": 90.0 + }, + "cameraType": { + "available": true, + "unit": "native CameraTypes enum", + "value": 0 + }, + "centerOfFocus": { + "available": null, + "notRead": "isDepthOfFieldEnabled is false or unreadable" + }, + "depthOfFieldBlur": { + "available": null, + "notRead": "isDepthOfFieldEnabled is false or unreadable" + }, + "groundOffset": { + "available": true, + "nativeUnit": "cm", + "unit": "mm", + "value": 0.0 + }, + "groundPosition": { + "available": null, + "notRead": "isGroundFlattened is false or unreadable" + }, + "groundRoughness": { + "available": null, + "notRead": "isGroundReflections is false or unreadable" + }, + "isDepthOfFieldEnabled": { + "available": true, + "value": false + }, + "isGroundDisplayed": { + "available": true, + "value": true + }, + "isGroundFlattened": { + "available": true, + "value": false + }, + "isGroundReflections": { + "available": true, + "value": false + }, + "lightAngle": { + "available": true, + "nativeUnit": "rad", + "unit": "deg", + "value": -0.0 + }, + "object": { + "available": true, + "objectType": "adsk::fusion::SceneSettings" + } + }, + "sceneWitnessComplete": false, + "settingsWritten": false, + "status": "DIAGNOSTIC_ONLY", + "viewport": { + "available": true, + "objectType": "adsk::core::Viewport" + } + }, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1778, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0018-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0018-fusion_run_modeling_script.json new file mode 100644 index 0000000..d1435e8 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0018-fusion_run_modeling_script.json @@ -0,0 +1,131 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"action\": \"witness\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"action\\\": \\\"witness\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235871.4715369, + "mutating": false, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"success\": true,\n \"witness\": {\n \"components\": [\n {\n \"attributes\": {\n \"requestedName\": \"Untitled\"\n },\n \"bodies\": [\n {\n \"bounds\": [\n [\n 0.0,\n 0.0,\n 0.0\n ],\n [\n 20.0,\n 30.0,\n 10.0\n ]\n ],\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"faces\": 6,\n \"mass\": 0.0162,\n \"material\": \"Aluminum\",\n \"name\": \"Block\",\n \"volume\": 6000.0\n }\n ],\n \"description\": \"\",\n \"material\": \"Aluminum\",\n \"name\": \"\",\n \"partNumber\": \"2026-10-05-14-31-00-614\",\n \"sketches\": [\n {\n \"curves\": 4,\n \"fullyConstrained\": false,\n \"name\": \"Base rectangle\",\n \"profiles\": 1\n }\n ]\n }\n ],\n \"instanceCount\": 1,\n \"instanceVolume\": 6000.0,\n \"occurrences\": [],\n \"parameters\": {\n \"d5\": \"width\",\n \"d6\": \"height\",\n \"d7\": \"depth\",\n \"d8\": \"0.0 deg\",\n \"depth\": \"10.0 mm\",\n \"height\": \"30.0 mm\",\n \"width\": \"20.0 mm\"\n },\n \"physical\": {\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"mass\": 0.0162,\n \"volume\": 6000.0\n },\n \"timelineCount\": 2\n }\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1802,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235873.2830126, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true, + "witness": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + } + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1802, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0019-fusion_mechanical.json b/docs/mechanical-verification/cp9-readonly/calls/0019-fusion_mechanical.json new file mode 100644 index 0000000..1bc193f --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0019-fusion_mechanical.json @@ -0,0 +1,257 @@ +{ + "target": "adom_drew2", + "verb": "fusion_mechanical", + "args": { + "op": "inspect" + }, + "requestJson": "{\"op\": \"inspect\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235873.2909608, + "mutating": false, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"bodies\": [\n {\n \"area\": 2200.0,\n \"bounds\": {\n \"max\": [\n 20.0,\n 30.0,\n 10.0\n ],\n \"min\": [\n 0.0,\n 0.0,\n 0.0\n ]\n },\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"componentId\": \"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\n \"edges\": 12,\n \"faces\": 6,\n \"isSolid\": true,\n \"mass\": 0.0162,\n \"name\": \"Block\",\n \"ref\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"objectType\": \"adsk::fusion::BRepBody\",\n \"occurrencePath\": null,\n \"occurrenceToken\": null,\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\n \"token\": \"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6YwMDANu8VROIAAAA\"\n },\n \"space\": \"component\",\n \"units\": {\n \"angle\": \"deg\",\n \"area\": \"mm2\",\n \"length\": \"mm\",\n \"mass\": \"kg\",\n \"volume\": \"mm3\"\n },\n \"volume\": 6000.0\n }\n ],\n \"complete\": true,\n \"components\": [\n {\n \"attributes\": {\n \"requestedName\": \"Untitled\"\n },\n \"bodyCount\": 1,\n \"description\": \"\",\n \"id\": \"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\n \"material\": {\n \"id\": \"PrismMaterial-002\",\n \"name\": \"Aluminum\"\n },\n \"name\": \"(Unsaved)\",\n \"partNumber\": \"2026-10-05-14-31-00-614\",\n \"ref\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"objectType\": \"adsk::fusion::Component\",\n \"occurrencePath\": null,\n \"occurrenceToken\": null,\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\n \"rootComponentId\": \"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\n \"token\": \"/v4BAAEAAwAAAAAAAAAAAAAA\"\n },\n \"sketchCount\": 1\n }\n ],\n \"configuration\": {\n \"complete\": true,\n \"configurationRowId\": \"\",\n \"configurationWritesEnabled\": false,\n \"configured\": false,\n \"configuredOccurrences\": [],\n \"isConfiguration\": false,\n \"readErrors\": [],\n \"scope\": \"loaded Design ordinary and reserved test rows; no activation or recompute\",\n \"table\": null,\n \"unsupported\": []\n },\n \"constructionAxes\": [],\n \"designIntent\": \"2\",\n \"document\": {\n \"creationId\": \"4f1af11b-0a58-4348-b81a-87502fd79fb2\",\n \"name\": \"Untitled\",\n \"saved\": false\n },\n \"features\": [\n {\n \"componentFeatureOrder\": 0,\n \"index\": 1,\n \"name\": \"Base extrusion\",\n \"ref\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"objectType\": \"adsk::fusion::ExtrudeFeature\",\n \"occurrencePath\": null,\n \"occurrenceToken\": null,\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\n \"token\": \"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IzMLAFj5FkqIAAAA\"\n },\n \"suppressed\": false,\n \"type\": \"adsk::fusion::ExtrudeFeature\"\n }\n ],\n \"health\": {\n \"complete\": true,\n \"computeSucceeded\": null,\n \"faults\": [],\n \"healthy\": true,\n \"suppressed\": [],\n \"timelineCount\": 2,\n \"unknown\": []\n },\n \"jointOrigins\": [],\n \"joints\": [],\n \"motionLinks\": [],\n \"occurrences\": [],\n \"parameters\": [\n {\n \"displayUnit\": \"mm\",\n \"expression\": \"20.0 mm\",\n \"name\": \"width\",\n \"unit\": \"mm\",\n \"value\": 20.0\n },\n {\n \"displayUnit\": \"mm\",\n \"expression\": \"30.0 mm\",\n \"name\": \"height\",\n \"unit\": \"mm\",\n \"value\": 30.0\n },\n {\n \"displayUnit\": \"mm\",\n \"expression\": \"10.0 mm\",\n \"name\": \"depth\",\n \"unit\": \"mm\",\n \"value\": 10.0\n },\n {\n \"displayUnit\": \"mm\",\n \"expression\": \"width\",\n \"name\": \"d5\",\n \"unit\": \"mm\",\n \"value\": 20.0\n },\n {\n \"displayUnit\": \"mm\",\n \"expression\": \"height\",\n \"name\": \"d6\",\n \"unit\": \"mm\",\n \"value\": 30.0\n },\n {\n \"displayUnit\": \"mm\",\n \"expression\": \"depth\",\n \"name\": \"d7\",\n \"unit\": \"mm\",\n \"value\": 10.0\n },\n {\n \"displayUnit\": \"deg\",\n \"expression\": \"0.0 deg\",\n \"name\": \"d8\",\n \"unit\": \"deg\",\n \"value\": 0.0\n }\n ],\n \"rigidGroups\": [],\n \"sketches\": [\n {\n \"componentId\": \"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\n \"fullyConstrained\": false,\n \"name\": \"Base rectangle\",\n \"profileCount\": 1,\n \"ref\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"objectType\": \"adsk::fusion::Sketch\",\n \"occurrencePath\": null,\n \"occurrenceToken\": null,\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\n \"token\": \"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IwNzAE9D84yIAAAA\"\n }\n }\n ],\n \"target\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"\n },\n \"units\": {\n \"angle\": \"deg\",\n \"area\": \"mm2\",\n \"length\": \"mm\",\n \"mass\": \"kg\",\n \"volume\": \"mm3\"\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 859,\n \"error\": \"\",\n \"operation\": \"inspect\",\n \"output\": \"{\\\"_hint\\\":\\\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\\\",\\\"_hintDocumentHygiene\\\":\\\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\\\",\\\"_timeoutHint\\\":\\\"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\\\",\\\"data\\\":{\\\"bodies\\\":[{\\\"area\\\":2200.0,\\\"bounds\\\":{\\\"max\\\":[20.0,30.0,10.0],\\\"min\\\":[0.0,0.0,0.0]},\\\"centerOfMass\\\":[10.0,15.0,5.0],\\\"componentId\\\":\\\"3400b1c0-7442-4474-9ad7-c5e6343280a7\\\",\\\"edges\\\":12,\\\"faces\\\":6,\\\"isSolid\\\":true,\\\"mass\\\":0.0162,\\\"name\\\":\\\"Block\\\",\\\"ref\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"objectType\\\":\\\"adsk::fusion::BRepBody\\\",\\\"occurrencePath\\\":null,\\\"occurrenceToken\\\":null,\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\",\\\"token\\\":\\\"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6YwMDANu8VROIAAAA\\\"},\\\"space\\\":\\\"component\\\",\\\"units\\\":{\\\"angle\\\":\\\"deg\\\",\\\"area\\\":\\\"mm2\\\",\\\"length\\\":\\\"mm\\\",\\\"mass\\\":\\\"kg\\\",\\\"volume\\\":\\\"mm3\\\"},\\\"volume\\\":6000.0}],\\\"complete\\\":true,\\\"components\\\":[{\\\"attributes\\\":{\\\"requestedName\\\":\\\"Untitled\\\"},\\\"bodyCount\\\":1,\\\"description\\\":\\\"\\\",\\\"id\\\":\\\"3400b1c0-7442-4474-9ad7-c5e6343280a7\\\",\\\"material\\\":{\\\"id\\\":\\\"PrismMaterial-002\\\",\\\"name\\\":\\\"Aluminum\\\"},\\\"name\\\":\\\"(Unsaved)\\\",\\\"partNumber\\\":\\\"2026-10-05-14-31-00-614\\\",\\\"ref\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"objectType\\\":\\\"adsk::fusion::Component\\\",\\\"occurrencePath\\\":null,\\\"occurrenceToken\\\":null,\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\",\\\"rootComponentId\\\":\\\"3400b1c0-7442-4474-9ad7-c5e6343280a7\\\",\\\"token\\\":\\\"/v4BAAEAAwAAAAAAAAAAAAAA\\\"},\\\"sketchCount\\\":1}],\\\"configuration\\\":{\\\"complete\\\":true,\\\"configurationRowId\\\":\\\"\\\",\\\"configurationWritesEnabled\\\":false,\\\"configured\\\":false,\\\"configuredOccurrences\\\":[],\\\"isConfiguration\\\":false,\\\"readErrors\\\":[],\\\"scope\\\":\\\"loaded Design ordinary and reserved test rows; no activation or recompute\\\",\\\"table\\\":null,\\\"unsupported\\\":[]},\\\"constructionAxes\\\":[],\\\"designIntent\\\":\\\"2\\\",\\\"document\\\":{\\\"creationId\\\":\\\"4f1af11b-0a58-4348-b81a-87502fd79fb2\\\",\\\"name\\\":\\\"Untitled\\\",\\\"saved\\\":false},\\\"features\\\":[{\\\"componentFeatureOrder\\\":0,\\\"index\\\":1,\\\"name\\\":\\\"Base extrusion\\\",\\\"ref\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"objectType\\\":\\\"adsk::fusion::ExtrudeFeature\\\",\\\"occurrencePath\\\":null,\\\"occurrenceToken\\\":null,\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\",\\\"token\\\":\\\"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IzMLAFj5FkqIAAAA\\\"},\\\"suppressed\\\":false,\\\"type\\\":\\\"adsk::fusion::ExtrudeFeature\\\"}],\\\"health\\\":{\\\"complete\\\":true,\\\"computeSucceeded\\\":null,\\\"faults\\\":[],\\\"healthy\\\":true,\\\"suppressed\\\":[],\\\"timelineCount\\\":2,\\\"unknown\\\":[]},\\\"jointOrigins\\\":[],\\\"joints\\\":[],\\\"motionLinks\\\":[],\\\"occurrences\\\":[],\\\"parameters\\\":[{\\\"displayUnit\\\":\\\"mm\\\",\\\"expression\\\":\\\"20.0 mm\\\",\\\"name\\\":\\\"width\\\",\\\"unit\\\":\\\"mm\\\",\\\"value\\\":20.0},{\\\"displayUnit\\\":\\\"mm\\\",\\\"expression\\\":\\\"30.0 mm\\\",\\\"name\\\":\\\"height\\\",\\\"unit\\\":\\\"mm\\\",\\\"value\\\":30.0},{\\\"displayUnit\\\":\\\"mm\\\",\\\"expression\\\":\\\"10.0 mm\\\",\\\"name\\\":\\\"depth\\\",\\\"unit\\\":\\\"mm\\\",\\\"value\\\":10.0},{\\\"displayUnit\\\":\\\"mm\\\",\\\"expression\\\":\\\"width\\\",\\\"name\\\":\\\"d5\\\",\\\"unit\\\":\\\"mm\\\",\\\"value\\\":20.0},{\\\"displayUnit\\\":\\\"mm\\\",\\\"expression\\\":\\\"height\\\",\\\"name\\\":\\\"d6\\\",\\\"unit\\\":\\\"mm\\\",\\\"value\\\":30.0},{\\\"displayUnit\\\":\\\"mm\\\",\\\"expression\\\":\\\"depth\\\",\\\"name\\\":\\\"d7\\\",\\\"unit\\\":\\\"mm\\\",\\\"value\\\":10.0},{\\\"displayUnit\\\":\\\"deg\\\",\\\"expression\\\":\\\"0.0 deg\\\",\\\"name\\\":\\\"d8\\\",\\\"unit\\\":\\\"deg\\\",\\\"value\\\":0.0}],\\\"rigidGroups\\\":[],\\\"sketches\\\":[{\\\"componentId\\\":\\\"3400b1c0-7442-4474-9ad7-c5e6343280a7\\\",\\\"fullyConstrained\\\":false,\\\"name\\\":\\\"Base rectangle\\\",\\\"profileCount\\\":1,\\\"ref\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"objectType\\\":\\\"adsk::fusion::Sketch\\\",\\\"occurrencePath\\\":null,\\\"occurrenceToken\\\":null,\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\",\\\"token\\\":\\\"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IwNzAE9D84yIAAAA\\\"}}],\\\"target\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\"},\\\"units\\\":{\\\"angle\\\":\\\"deg\\\",\\\"area\\\":\\\"mm2\\\",\\\"length\\\":\\\"mm\\\",\\\"mass\\\":\\\"kg\\\",\\\"volume\\\":\\\"mm3\\\"}},\\\"documentHygiene\\\":{\\\"documents\\\":[{\\\"isModified\\\":false,\\\"isSaved\\\":false,\\\"name\\\":\\\"Untitled\\\",\\\"needsSave\\\":false,\\\"saveStateKnown\\\":true},{\\\"isModified\\\":true,\\\"isSaved\\\":false,\\\"name\\\":\\\"Untitled\\\",\\\"needsSave\\\":true,\\\"saveStateKnown\\\":true}],\\\"inspectionComplete\\\":true,\\\"needsSaveCount\\\":1,\\\"openDocumentCount\\\":2,\\\"ownership\\\":\\\"notInferred\\\",\\\"recoveryHealth\\\":\\\"notInspected\\\"},\\\"operation\\\":\\\"inspect\\\",\\\"sessionId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\",\\\"shell\\\":\\\"rust\\\",\\\"statusVerb\\\":\\\"fusion_get_app_state\\\",\\\"success\\\":true,\\\"timeoutSeconds\\\":60}\",\n \"schemaVersion\": 1,\n \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 60\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235874.15848, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "bodies": [ + { + "area": 2200.0, + "bounds": { + "max": [ + 20.0, + 30.0, + 10.0 + ], + "min": [ + 0.0, + 0.0, + 0.0 + ] + }, + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "componentId": "3400b1c0-7442-4474-9ad7-c5e6343280a7", + "edges": 12, + "faces": 6, + "isSolid": true, + "mass": 0.0162, + "name": "Block", + "ref": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "objectType": "adsk::fusion::BRepBody", + "occurrencePath": null, + "occurrenceToken": null, + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed", + "token": "/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6YwMDANu8VROIAAAA" + }, + "space": "component", + "units": { + "angle": "deg", + "area": "mm2", + "length": "mm", + "mass": "kg", + "volume": "mm3" + }, + "volume": 6000.0 + } + ], + "complete": true, + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodyCount": 1, + "description": "", + "id": "3400b1c0-7442-4474-9ad7-c5e6343280a7", + "material": { + "id": "PrismMaterial-002", + "name": "Aluminum" + }, + "name": "(Unsaved)", + "partNumber": "2026-10-05-14-31-00-614", + "ref": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "objectType": "adsk::fusion::Component", + "occurrencePath": null, + "occurrenceToken": null, + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed", + "rootComponentId": "3400b1c0-7442-4474-9ad7-c5e6343280a7", + "token": "/v4BAAEAAwAAAAAAAAAAAAAA" + }, + "sketchCount": 1 + } + ], + "configuration": { + "complete": true, + "configurationRowId": "", + "configurationWritesEnabled": false, + "configured": false, + "configuredOccurrences": [], + "isConfiguration": false, + "readErrors": [], + "scope": "loaded Design ordinary and reserved test rows; no activation or recompute", + "table": null, + "unsupported": [] + }, + "constructionAxes": [], + "designIntent": "2", + "document": { + "creationId": "4f1af11b-0a58-4348-b81a-87502fd79fb2", + "name": "Untitled", + "saved": false + }, + "features": [ + { + "componentFeatureOrder": 0, + "index": 1, + "name": "Base extrusion", + "ref": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "objectType": "adsk::fusion::ExtrudeFeature", + "occurrencePath": null, + "occurrenceToken": null, + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed", + "token": "/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IzMLAFj5FkqIAAAA" + }, + "suppressed": false, + "type": "adsk::fusion::ExtrudeFeature" + } + ], + "health": { + "complete": true, + "computeSucceeded": null, + "faults": [], + "healthy": true, + "suppressed": [], + "timelineCount": 2, + "unknown": [] + }, + "jointOrigins": [], + "joints": [], + "motionLinks": [], + "occurrences": [], + "parameters": [ + { + "displayUnit": "mm", + "expression": "20.0 mm", + "name": "width", + "unit": "mm", + "value": 20.0 + }, + { + "displayUnit": "mm", + "expression": "30.0 mm", + "name": "height", + "unit": "mm", + "value": 30.0 + }, + { + "displayUnit": "mm", + "expression": "10.0 mm", + "name": "depth", + "unit": "mm", + "value": 10.0 + }, + { + "displayUnit": "mm", + "expression": "width", + "name": "d5", + "unit": "mm", + "value": 20.0 + }, + { + "displayUnit": "mm", + "expression": "height", + "name": "d6", + "unit": "mm", + "value": 30.0 + }, + { + "displayUnit": "mm", + "expression": "depth", + "name": "d7", + "unit": "mm", + "value": 10.0 + }, + { + "displayUnit": "deg", + "expression": "0.0 deg", + "name": "d8", + "unit": "deg", + "value": 0.0 + } + ], + "rigidGroups": [], + "sketches": [ + { + "componentId": "3400b1c0-7442-4474-9ad7-c5e6343280a7", + "fullyConstrained": false, + "name": "Base rectangle", + "profileCount": 1, + "ref": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "objectType": "adsk::fusion::Sketch", + "occurrencePath": null, + "occurrenceToken": null, + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed", + "token": "/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IwNzAE9D84yIAAAA" + } + } + ], + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + }, + "units": { + "angle": "deg", + "area": "mm2", + "length": "mm", + "mass": "kg", + "volume": "mm3" + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 859, + "error": "", + "operation": "inspect", + "output": "{\"_hint\":\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\"_hintDocumentHygiene\":\"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\"_timeoutHint\":\"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\"data\":{\"bodies\":[{\"area\":2200.0,\"bounds\":{\"max\":[20.0,30.0,10.0],\"min\":[0.0,0.0,0.0]},\"centerOfMass\":[10.0,15.0,5.0],\"componentId\":\"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\"edges\":12,\"faces\":6,\"isSolid\":true,\"mass\":0.0162,\"name\":\"Block\",\"ref\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"objectType\":\"adsk::fusion::BRepBody\",\"occurrencePath\":null,\"occurrenceToken\":null,\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\"token\":\"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6YwMDANu8VROIAAAA\"},\"space\":\"component\",\"units\":{\"angle\":\"deg\",\"area\":\"mm2\",\"length\":\"mm\",\"mass\":\"kg\",\"volume\":\"mm3\"},\"volume\":6000.0}],\"complete\":true,\"components\":[{\"attributes\":{\"requestedName\":\"Untitled\"},\"bodyCount\":1,\"description\":\"\",\"id\":\"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\"material\":{\"id\":\"PrismMaterial-002\",\"name\":\"Aluminum\"},\"name\":\"(Unsaved)\",\"partNumber\":\"2026-10-05-14-31-00-614\",\"ref\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"objectType\":\"adsk::fusion::Component\",\"occurrencePath\":null,\"occurrenceToken\":null,\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\"rootComponentId\":\"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\"token\":\"/v4BAAEAAwAAAAAAAAAAAAAA\"},\"sketchCount\":1}],\"configuration\":{\"complete\":true,\"configurationRowId\":\"\",\"configurationWritesEnabled\":false,\"configured\":false,\"configuredOccurrences\":[],\"isConfiguration\":false,\"readErrors\":[],\"scope\":\"loaded Design ordinary and reserved test rows; no activation or recompute\",\"table\":null,\"unsupported\":[]},\"constructionAxes\":[],\"designIntent\":\"2\",\"document\":{\"creationId\":\"4f1af11b-0a58-4348-b81a-87502fd79fb2\",\"name\":\"Untitled\",\"saved\":false},\"features\":[{\"componentFeatureOrder\":0,\"index\":1,\"name\":\"Base extrusion\",\"ref\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"objectType\":\"adsk::fusion::ExtrudeFeature\",\"occurrencePath\":null,\"occurrenceToken\":null,\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\"token\":\"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IzMLAFj5FkqIAAAA\"},\"suppressed\":false,\"type\":\"adsk::fusion::ExtrudeFeature\"}],\"health\":{\"complete\":true,\"computeSucceeded\":null,\"faults\":[],\"healthy\":true,\"suppressed\":[],\"timelineCount\":2,\"unknown\":[]},\"jointOrigins\":[],\"joints\":[],\"motionLinks\":[],\"occurrences\":[],\"parameters\":[{\"displayUnit\":\"mm\",\"expression\":\"20.0 mm\",\"name\":\"width\",\"unit\":\"mm\",\"value\":20.0},{\"displayUnit\":\"mm\",\"expression\":\"30.0 mm\",\"name\":\"height\",\"unit\":\"mm\",\"value\":30.0},{\"displayUnit\":\"mm\",\"expression\":\"10.0 mm\",\"name\":\"depth\",\"unit\":\"mm\",\"value\":10.0},{\"displayUnit\":\"mm\",\"expression\":\"width\",\"name\":\"d5\",\"unit\":\"mm\",\"value\":20.0},{\"displayUnit\":\"mm\",\"expression\":\"height\",\"name\":\"d6\",\"unit\":\"mm\",\"value\":30.0},{\"displayUnit\":\"mm\",\"expression\":\"depth\",\"name\":\"d7\",\"unit\":\"mm\",\"value\":10.0},{\"displayUnit\":\"deg\",\"expression\":\"0.0 deg\",\"name\":\"d8\",\"unit\":\"deg\",\"value\":0.0}],\"rigidGroups\":[],\"sketches\":[{\"componentId\":\"3400b1c0-7442-4474-9ad7-c5e6343280a7\",\"fullyConstrained\":false,\"name\":\"Base rectangle\",\"profileCount\":1,\"ref\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"objectType\":\"adsk::fusion::Sketch\",\"occurrencePath\":null,\"occurrenceToken\":null,\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\",\"token\":\"/v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwoPHX9xX3/A7sGLnqYgk4Vg3oAojBWMzhcTklBTLtDRDXQNT00RdE0tzU10LC3MLXbPUFEPTREPzVKA0WC3QDAY0ALbPQAFsHch6IwNzAE9D84yIAAAA\"}}],\"target\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"},\"units\":{\"angle\":\"deg\",\"area\":\"mm2\",\"length\":\"mm\",\"mass\":\"kg\",\"volume\":\"mm3\"}},\"documentHygiene\":{\"documents\":[{\"isModified\":false,\"isSaved\":false,\"name\":\"Untitled\",\"needsSave\":false,\"saveStateKnown\":true},{\"isModified\":true,\"isSaved\":false,\"name\":\"Untitled\",\"needsSave\":true,\"saveStateKnown\":true}],\"inspectionComplete\":true,\"needsSaveCount\":1,\"openDocumentCount\":2,\"ownership\":\"notInferred\",\"recoveryHealth\":\"notInspected\"},\"operation\":\"inspect\",\"sessionId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\"shell\":\"rust\",\"statusVerb\":\"fusion_get_app_state\",\"success\":true,\"timeoutSeconds\":60}", + "schemaVersion": 1, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60 + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0020-fusion_mechanical.json b/docs/mechanical-verification/cp9-readonly/calls/0020-fusion_mechanical.json new file mode 100644 index 0000000..306dd9c --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0020-fusion_mechanical.json @@ -0,0 +1,59 @@ +{ + "target": "adom_drew2", + "verb": "fusion_mechanical", + "args": { + "op": "export", + "format": "f3d", + "outputPath": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + }, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "operationId": "20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35" + }, + "requestJson": "{\"op\": \"export\", \"format\": \"f3d\", \"outputPath\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\", \"target\": {\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"}, \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\", \"operationId\": \"20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235874.160165, + "mutating": true, + "stdout": "{\n \"_timeoutHint\": \"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"externalEffect\": \"file_published\",\n \"fileSize\": 65067,\n \"format\": \"f3d\",\n \"modelRollbackApplies\": false,\n \"outputPath\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"overwrite\": false,\n \"published\": true,\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\",\n \"target\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"revision\": \"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"\n }\n },\n \"durability\": {\n \"phase\": \"succeeded\",\n \"recordedSessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\n \"storageVersion\": 1,\n \"terminalRecorded\": true\n },\n \"durationMs\": 1190,\n \"error\": \"\",\n \"operation\": \"export\",\n \"operationId\": \"20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35\",\n \"output\": \"{\\\"_timeoutHint\\\":\\\"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\\\",\\\"data\\\":{\\\"externalEffect\\\":\\\"file_published\\\",\\\"fileSize\\\":65067,\\\"format\\\":\\\"f3d\\\",\\\"modelRollbackApplies\\\":false,\\\"outputPath\\\":\\\"C:\\\\\\\\Users\\\\\\\\drew\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Temp\\\\\\\\adom-fusion-ralph\\\\\\\\20261005T213050Z-422781656d64490da02934d1757322ed\\\\\\\\01-scene_diagnostic-fad76a63.f3d\\\",\\\"overwrite\\\":false,\\\"published\\\":true,\\\"sha256\\\":\\\"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\\\",\\\"target\\\":{\\\"documentId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\",\\\"revision\\\":\\\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\\\"}},\\\"durability\\\":{\\\"phase\\\":\\\"succeeded\\\",\\\"recordedSessionId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\",\\\"storageVersion\\\":1,\\\"terminalRecorded\\\":true},\\\"operation\\\":\\\"export\\\",\\\"operationId\\\":\\\"20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35\\\",\\\"sessionId\\\":\\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\",\\\"shell\\\":\\\"rust\\\",\\\"statusVerb\\\":\\\"fusion_get_app_state\\\",\\\"success\\\":true,\\\"timeoutSeconds\\\":60}\",\n \"schemaVersion\": 1,\n \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 60\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235875.359536, + "raw": { + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "externalEffect": "file_published", + "fileSize": 65067, + "format": "f3d", + "modelRollbackApplies": false, + "outputPath": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "overwrite": false, + "published": true, + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + } + }, + "durability": { + "phase": "succeeded", + "recordedSessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "storageVersion": 1, + "terminalRecorded": true + }, + "durationMs": 1190, + "error": "", + "operation": "export", + "operationId": "20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35", + "output": "{\"_timeoutHint\":\"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\"data\":{\"externalEffect\":\"file_published\",\"fileSize\":65067,\"format\":\"f3d\",\"modelRollbackApplies\":false,\"outputPath\":\"C:\\\\Users\\\\drew\\\\AppData\\\\Local\\\\Temp\\\\adom-fusion-ralph\\\\20261005T213050Z-422781656d64490da02934d1757322ed\\\\01-scene_diagnostic-fad76a63.f3d\",\"overwrite\":false,\"published\":true,\"sha256\":\"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\",\"target\":{\"documentId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\"revision\":\"3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed\"}},\"durability\":{\"phase\":\"succeeded\",\"recordedSessionId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\"storageVersion\":1,\"terminalRecorded\":true},\"operation\":\"export\",\"operationId\":\"20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35\",\"sessionId\":\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\",\"shell\":\"rust\",\"statusVerb\":\"fusion_get_app_state\",\"success\":true,\"timeoutSeconds\":60}", + "schemaVersion": 1, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60 + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0021-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0021-fusion_run_modeling_script.json new file mode 100644 index 0000000..8d412c7 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0021-fusion_run_modeling_script.json @@ -0,0 +1,243 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\", \"action\": \"checkpoint\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"path\\\": \\\"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\\\", \\\"action\\\": \\\"checkpoint\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235875.3602197, + "mutating": true, + "stdout": "{\n \"_hint\": \"Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"_timing\": {\n \"addinSec\": 0.61,\n \"dialogsSec\": 0.61,\n \"postSec\": 0.96,\n \"preSec\": 0.02,\n \"scope\": \"one exe pass-through of this verb only: excludes the relay hop, ab's queue, and any earlier pass-through a composite verb (routing, tour) made before this one, so the caller's wall clock can be longer\",\n \"totalSec\": 2.2\n },\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"after\": {\n \"components\": [\n {\n \"attributes\": {\n \"requestedName\": \"Untitled\"\n },\n \"bodies\": [\n {\n \"bounds\": [\n [\n 0.0,\n 0.0,\n 0.0\n ],\n [\n 20.0,\n 30.0,\n 10.0\n ]\n ],\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"faces\": 6,\n \"mass\": 0.0162,\n \"material\": \"Aluminum\",\n \"name\": \"Block\",\n \"volume\": 6000.0\n }\n ],\n \"description\": \"\",\n \"material\": \"Aluminum\",\n \"name\": \"\",\n \"partNumber\": \"2026-10-05-14-31-00-614\",\n \"sketches\": [\n {\n \"curves\": 4,\n \"fullyConstrained\": false,\n \"name\": \"Base rectangle\",\n \"profiles\": 1\n }\n ]\n }\n ],\n \"instanceCount\": 1,\n \"instanceVolume\": 6000.0,\n \"occurrences\": [],\n \"parameters\": {\n \"d5\": \"width\",\n \"d6\": \"height\",\n \"d7\": \"depth\",\n \"d8\": \"0.0 deg\",\n \"depth\": \"10.0 mm\",\n \"height\": \"30.0 mm\",\n \"width\": \"20.0 mm\"\n },\n \"physical\": {\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"mass\": 0.0162,\n \"volume\": 6000.0\n },\n \"timelineCount\": 2\n },\n \"archive\": {\n \"bytes\": 65067,\n \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\"\n },\n \"before\": {\n \"components\": [\n {\n \"attributes\": {\n \"requestedName\": \"Untitled\"\n },\n \"bodies\": [\n {\n \"bounds\": [\n [\n 0.0,\n 0.0,\n 0.0\n ],\n [\n 20.0,\n 30.0,\n 10.0\n ]\n ],\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"faces\": 6,\n \"mass\": 0.0162,\n \"material\": \"Aluminum\",\n \"name\": \"Block\",\n \"volume\": 6000.0\n }\n ],\n \"description\": \"\",\n \"material\": \"Aluminum\",\n \"name\": \"\",\n \"partNumber\": \"2026-10-05-14-31-00-614\",\n \"sketches\": [\n {\n \"curves\": 4,\n \"fullyConstrained\": false,\n \"name\": \"Base rectangle\",\n \"profiles\": 1\n }\n ]\n }\n ],\n \"instanceCount\": 1,\n \"instanceVolume\": 6000.0,\n \"occurrences\": [],\n \"parameters\": {\n \"d5\": \"width\",\n \"d6\": \"height\",\n \"d7\": \"depth\",\n \"d8\": \"0.0 deg\",\n \"depth\": \"10.0 mm\",\n \"height\": \"30.0 mm\",\n \"width\": \"20.0 mm\"\n },\n \"physical\": {\n \"centerOfMass\": [\n 10.0,\n 15.0,\n 5.0\n ],\n \"mass\": 0.0162,\n \"volume\": 6000.0\n },\n \"timelineCount\": 2\n },\n \"reopened\": {\n \"checkpoint\": {\n \"bytes\": 65067,\n \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\"\n },\n \"closed\": false,\n \"creationId\": \"6f7c8295-af36-44c4-bbbc-fcdff5932a5e\",\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\",\n \"name\": \"Untitled\",\n \"role\": \"01-scene_diagnostic-reopened\"\n },\n \"source\": {\n \"checkpoint\": {\n \"bytes\": 65067,\n \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\"\n },\n \"closed\": false,\n \"creationId\": \"4f1af11b-0a58-4348-b81a-87502fd79fb2\",\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"name\": \"Untitled\",\n \"role\": \"01-scene_diagnostic\"\n },\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 2,\n \"openDocumentCount\": 3,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 2310,\n \"error\": \"\",\n \"output\": \"Workspace Deactivated event received\\nToolbar State: event: workspaceDeactivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\\nWorkspace Activated: Design\\nToolbar State: event: workspaceActivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235877.6798115, + "raw": { + "_hint": "Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "_timing": { + "addinSec": 0.61, + "dialogsSec": 0.61, + "postSec": 0.96, + "preSec": 0.02, + "scope": "one exe pass-through of this verb only: excludes the relay hop, ab's queue, and any earlier pass-through a composite verb (routing, tour) made before this one, so the caller's wall clock can be longer", + "totalSec": 2.2 + }, + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "after": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + }, + "archive": { + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764" + }, + "before": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + }, + "reopened": { + "checkpoint": { + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764" + }, + "closed": false, + "creationId": "6f7c8295-af36-44c4-bbbc-fcdff5932a5e", + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "name": "Untitled", + "role": "01-scene_diagnostic-reopened" + }, + "source": { + "checkpoint": { + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764" + }, + "closed": false, + "creationId": "4f1af11b-0a58-4348-b81a-87502fd79fb2", + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "name": "Untitled", + "role": "01-scene_diagnostic" + }, + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 2, + "openDocumentCount": 3, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 2310, + "error": "", + "output": "Workspace Deactivated event received\nToolbar State: event: workspaceDeactivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\nWorkspace Activated: Design\nToolbar State: event: workspaceActivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0022-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0022-fusion_run_modeling_script.json new file mode 100644 index 0000000..f3ebb21 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0022-fusion_run_modeling_script.json @@ -0,0 +1,72 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\", \"action\": \"file\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"path\\\": \\\"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\\\", \\\"action\\\": \\\"file\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235877.6868026, + "mutating": false, + "stdout": "{\n \"_hint\": \"Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"base64\": \"UEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAYAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAoAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vQnJlcHMuQmxvYlBhcnRzL1BLAwQKAAAAAADnq0VdAAAAAAAAAAAAAAAAKQAAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0ltYWdlcy5CbG9iUGFydHMvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAwAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUHJvdGVpbkFzc2V0cy5CbG9iUGFydHMvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAA7AAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRGVzaWduQ29uZmlndXJhdGlvblRhYmxlLkJsb2JQYXJ0cy9QSwMECgAAAAAA56tFXQAAAAAAAAAAAAAAACcAAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9PR1MuQmxvYkZvbGRlci9QSwMECgAAAAAA56tFXQAAAAAAAAAAAAAAADEAAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25EZXNpZ25TZWdtZW50VHlwZTEvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAuAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRnVzaW9uQUNUU2VnbWVudFR5cGUxL1BLAwQKAAAAAADnq0VdAAAAAAAAAAAAAAAAMgAAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0Z1c2lvbkJyb3dzZXJTZWdtZW50VHlwZTEvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAhAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUHJldmlld3MvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAArAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vT0dTLkJsb2JGb2xkZXIvT0dTL1BLAwQKAAAAAADnq0VdAAAAAAAAAAAAAAAAOAAAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL09HUy5CbG9iRm9sZGVyL09HUy9EZWZhdWx0U2NlbmUvUEsDBBQAAABdAOer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AAAADgAAAAAAAAAAAAAAtIELBQAAUHJvcGVydGllcy5kYXRQSwECLgMUAAAAXQDnq0Vd01i6QVEBAABNAgAAJAAAAAAAAAAAAAAAtIE7BQAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vTWFuaWZlc3QuZGF0UEsBAi4DFAAAAF0A56tFXWLX3P1yBgAAECQAAFYAAAAAAAAAAAAAALSBzgYAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0JyZXBzLkJsb2JQYXJ0cy9CUkVQLjQ4NTFiYmEwLWI2NWEtNDZmYi04YWZmLTY5N2FlZjE3OTFjZC5zbWJoUEsBAi4DFAAAAF0A56tFXXFtFUgRBwAA1C4AAFUAAAAAAAAAAAAAALSBtA0AAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0JyZXBzLkJsb2JQYXJ0cy9CUkVQLjE4YjFlOTUxLTU0NjktNDhjZC1iYWMyLTYwNWMxYjRhOTZkNi5zbWJQSwECLgMUAAAAXQDnq0VduQUyKPgxAADqPwAAaQAAAAAAAAAAAAAAtIE4FQAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUHJvdGVpbkFzc2V0cy5CbG9iUGFydHMvUHJvdGVpbkFzc2V0LmMzZjBjOWNmLTE2M2UtNDQ2Ni1hNDg5LTVmYzhkNTk0NWJjMy5wcm90ZWluUEsBAi4DFAAAAAAA56tFXUO/pqMCAAAAAgAAAIIAAAAAAAAAAAAAALSBt0cAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0Rlc2lnbkNvbmZpZ3VyYXRpb25UYWJsZS5CbG9iUGFydHMvRGVzaWduQ29uZmlndXJhdGlvblJ1bGUuNmFmMTc1NWUtODE0Ny00N2I0LWEyYTMtNjNmZDc4OWI2YjczLmRzZ2NmZ3J1bGVQSwECLgMUAAAAAADnq0VdQ7+mowIAAAACAAAAfwAAAAAAAAAAAAAAtIFZSAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRGVzaWduQ29uZmlndXJhdGlvblRhYmxlLkJsb2JQYXJ0cy9EZXNpZ25Db25maWd1cmF0aW9uVGFibGUuNGM3ZGFjMjUtYWU2Ny00MzE0LTgyNjMtMWI1MzZiMWVkMmNmLmRzZ2NmZ1BLAQIuAxQAAABdAOerRV1d1CLOEBwAAIqRAAA/AAAAAAAAAAAAAAC0gfhIAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25EZXNpZ25TZWdtZW50VHlwZTEvQnVsa1N0cmVhbS5kYXRQSwECLgMUAAAAXQDnq0VdJZHUh0weAAAFYQAAPwAAAAAAAAAAAAAAtIFlZQAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRnVzaW9uRGVzaWduU2VnbWVudFR5cGUxL01ldGFTdHJlYW0uZGF0UEsBAi4DFAAAAF0A56tFXUcNAmEXBAAAmAwAADwAAAAAAAAAAAAAALSBDoQAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0Z1c2lvbkFDVFNlZ21lbnRUeXBlMS9CdWxrU3RyZWFtLmRhdFBLAQIuAxQAAABdAOerRV3tCEiqqAIAACEHAAA8AAAAAAAAAAAAAAC0gX+IAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25BQ1RTZWdtZW50VHlwZTEvTWV0YVN0cmVhbS5kYXRQSwECLgMUAAAAXQDnq0VdCt2RbV0DAABFFQAAQAAAAAAAAAAAAAAAtIGBiwAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRnVzaW9uQnJvd3NlclNlZ21lbnRUeXBlMS9CdWxrU3RyZWFtLmRhdFBLAQIuAxQAAABdAOerRV1x1OT3HgYAANoVAABAAAAAAAAAAAAAAAC0gTyPAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25Ccm93c2VyU2VnbWVudFR5cGUxL01ldGFTdHJlYW0uZGF0UEsBAi4DFAAAAF0A56tFXe5644aoDQAASg4AACoAAAAAAAAAAAAAALSBuJUAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL1ByZXZpZXdzL3NtYWxsLnBuZ1BLAQIuAxQAAABdAOerRV1FbYPsgTsAAHZGAAAoAAAAAAAAAAAAAAC0gaijAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9QcmV2aWV3cy9iaWcucG5nUEsBAi4DFAAAAF0A56tFXcizsbnJDgAAY5gAAD0AAAAAAAAAAAAAALSBb98AAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL09HUy5CbG9iRm9sZGVyL09HUy9EZWZhdWx0U2NlbmUvd29ybGRQSwECLgMUAAAAXQDnq0Vd5UCvKSUBAAAMBQAARwAAAAAAAAAAAAAAtIGT7gAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vT0dTLkJsb2JGb2xkZXIvT0dTL0RlZmF1bHRTY2VuZS9zdHJlYW1fbWVzaF8wMDBQSwECLgMUAAAAXQDnq0VdHN/w0AsBAADQBQAARwAAAAAAAAAAAAAAtIEd8AAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vT0dTLkJsb2JGb2xkZXIvT0dTL0RlZmF1bHRTY2VuZS9GdXNpb25fbWVzaF8wMDBQSwUGAAAAAB8AHwCIDAAAjfEAAAAA\",\n \"bytes\": 65067,\n \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 2,\n \"openDocumentCount\": 3,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1790,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235879.4860241, + "raw": { + "_hint": "Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 3 open, 2 need saving (Untitled, Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "base64": "UEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAYAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAoAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vQnJlcHMuQmxvYlBhcnRzL1BLAwQKAAAAAADnq0VdAAAAAAAAAAAAAAAAKQAAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0ltYWdlcy5CbG9iUGFydHMvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAwAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUHJvdGVpbkFzc2V0cy5CbG9iUGFydHMvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAA7AAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRGVzaWduQ29uZmlndXJhdGlvblRhYmxlLkJsb2JQYXJ0cy9QSwMECgAAAAAA56tFXQAAAAAAAAAAAAAAACcAAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9PR1MuQmxvYkZvbGRlci9QSwMECgAAAAAA56tFXQAAAAAAAAAAAAAAADEAAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25EZXNpZ25TZWdtZW50VHlwZTEvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAuAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRnVzaW9uQUNUU2VnbWVudFR5cGUxL1BLAwQKAAAAAADnq0VdAAAAAAAAAAAAAAAAMgAAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0Z1c2lvbkJyb3dzZXJTZWdtZW50VHlwZTEvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAAhAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUHJldmlld3MvUEsDBAoAAAAAAOerRV0AAAAAAAAAAAAAAAArAAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vT0dTLkJsb2JGb2xkZXIvT0dTL1BLAwQKAAAAAADnq0VdAAAAAAAAAAAAAAAAOAAAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL09HUy5CbG9iRm9sZGVyL09HUy9EZWZhdWx0U2NlbmUvUEsDBBQAAABdAOer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AAAADgAAAAAAAAAAAAAAtIELBQAAUHJvcGVydGllcy5kYXRQSwECLgMUAAAAXQDnq0Vd01i6QVEBAABNAgAAJAAAAAAAAAAAAAAAtIE7BQAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vTWFuaWZlc3QuZGF0UEsBAi4DFAAAAF0A56tFXWLX3P1yBgAAECQAAFYAAAAAAAAAAAAAALSBzgYAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0JyZXBzLkJsb2JQYXJ0cy9CUkVQLjQ4NTFiYmEwLWI2NWEtNDZmYi04YWZmLTY5N2FlZjE3OTFjZC5zbWJoUEsBAi4DFAAAAF0A56tFXXFtFUgRBwAA1C4AAFUAAAAAAAAAAAAAALSBtA0AAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0JyZXBzLkJsb2JQYXJ0cy9CUkVQLjE4YjFlOTUxLTU0NjktNDhjZC1iYWMyLTYwNWMxYjRhOTZkNi5zbWJQSwECLgMUAAAAXQDnq0VduQUyKPgxAADqPwAAaQAAAAAAAAAAAAAAtIE4FQAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vUHJvdGVpbkFzc2V0cy5CbG9iUGFydHMvUHJvdGVpbkFzc2V0LmMzZjBjOWNmLTE2M2UtNDQ2Ni1hNDg5LTVmYzhkNTk0NWJjMy5wcm90ZWluUEsBAi4DFAAAAAAA56tFXUO/pqMCAAAAAgAAAIIAAAAAAAAAAAAAALSBt0cAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0Rlc2lnbkNvbmZpZ3VyYXRpb25UYWJsZS5CbG9iUGFydHMvRGVzaWduQ29uZmlndXJhdGlvblJ1bGUuNmFmMTc1NWUtODE0Ny00N2I0LWEyYTMtNjNmZDc4OWI2YjczLmRzZ2NmZ3J1bGVQSwECLgMUAAAAAADnq0VdQ7+mowIAAAACAAAAfwAAAAAAAAAAAAAAtIFZSAAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRGVzaWduQ29uZmlndXJhdGlvblRhYmxlLkJsb2JQYXJ0cy9EZXNpZ25Db25maWd1cmF0aW9uVGFibGUuNGM3ZGFjMjUtYWU2Ny00MzE0LTgyNjMtMWI1MzZiMWVkMmNmLmRzZ2NmZ1BLAQIuAxQAAABdAOerRV1d1CLOEBwAAIqRAAA/AAAAAAAAAAAAAAC0gfhIAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25EZXNpZ25TZWdtZW50VHlwZTEvQnVsa1N0cmVhbS5kYXRQSwECLgMUAAAAXQDnq0VdJZHUh0weAAAFYQAAPwAAAAAAAAAAAAAAtIFlZQAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRnVzaW9uRGVzaWduU2VnbWVudFR5cGUxL01ldGFTdHJlYW0uZGF0UEsBAi4DFAAAAF0A56tFXUcNAmEXBAAAmAwAADwAAAAAAAAAAAAAALSBDoQAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL0Z1c2lvbkFDVFNlZ21lbnRUeXBlMS9CdWxrU3RyZWFtLmRhdFBLAQIuAxQAAABdAOerRV3tCEiqqAIAACEHAAA8AAAAAAAAAAAAAAC0gX+IAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25BQ1RTZWdtZW50VHlwZTEvTWV0YVN0cmVhbS5kYXRQSwECLgMUAAAAXQDnq0VdCt2RbV0DAABFFQAAQAAAAAAAAAAAAAAAtIGBiwAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vRnVzaW9uQnJvd3NlclNlZ21lbnRUeXBlMS9CdWxrU3RyZWFtLmRhdFBLAQIuAxQAAABdAOerRV1x1OT3HgYAANoVAABAAAAAAAAAAAAAAAC0gTyPAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9GdXNpb25Ccm93c2VyU2VnbWVudFR5cGUxL01ldGFTdHJlYW0uZGF0UEsBAi4DFAAAAF0A56tFXe5644aoDQAASg4AACoAAAAAAAAAAAAAALSBuJUAAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL1ByZXZpZXdzL3NtYWxsLnBuZ1BLAQIuAxQAAABdAOerRV1FbYPsgTsAAHZGAAAoAAAAAAAAAAAAAAC0gaijAABGdXNpb25Bc3NldE5hbWVbQWN0aXZlXS9QcmV2aWV3cy9iaWcucG5nUEsBAi4DFAAAAF0A56tFXcizsbnJDgAAY5gAAD0AAAAAAAAAAAAAALSBb98AAEZ1c2lvbkFzc2V0TmFtZVtBY3RpdmVdL09HUy5CbG9iRm9sZGVyL09HUy9EZWZhdWx0U2NlbmUvd29ybGRQSwECLgMUAAAAXQDnq0Vd5UCvKSUBAAAMBQAARwAAAAAAAAAAAAAAtIGT7gAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vT0dTLkJsb2JGb2xkZXIvT0dTL0RlZmF1bHRTY2VuZS9zdHJlYW1fbWVzaF8wMDBQSwECLgMUAAAAXQDnq0VdHN/w0AsBAADQBQAARwAAAAAAAAAAAAAAtIEd8AAARnVzaW9uQXNzZXROYW1lW0FjdGl2ZV0vT0dTLkJsb2JGb2xkZXIvT0dTL0RlZmF1bHRTY2VuZS9GdXNpb25fbWVzaF8wMDBQSwUGAAAAAB8AHwCIDAAAjfEAAAAA", + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 2, + "openDocumentCount": 3, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1790, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0023-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0023-fusion_run_modeling_script.json new file mode 100644 index 0000000..89860a2 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0023-fusion_run_modeling_script.json @@ -0,0 +1,63 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\", \"action\": \"close\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\\\", \\\"action\\\": \\\"close\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235879.4927967, + "mutating": true, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"closed\": true,\n \"closedDocumentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 2148,\n \"error\": \"\",\n \"output\": \"Workspace Deactivated event received\\nToolbar State: event: workspaceDeactivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\\nWorkspace Activated: Design\\nToolbar State: event: workspaceActivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235881.6493092, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 2148, + "error": "", + "output": "Workspace Deactivated event received\nToolbar State: event: workspaceDeactivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\nWorkspace Activated: Design\nToolbar State: event: workspaceActivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0024-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0024-fusion_run_modeling_script.json new file mode 100644 index 0000000..49cfc61 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0024-fusion_run_modeling_script.json @@ -0,0 +1,62 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"action\": \"activate\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"action\\\": \\\"activate\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235881.6499941, + "mutating": true, + "stdout": "{\n \"_hint\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_hintDocumentHygiene\": \"Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n },\n {\n \"isModified\": true,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": true,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 1,\n \"openDocumentCount\": 2,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1836,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235883.49537, + "raw": { + "_hint": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud. Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_hintDocumentHygiene": "Document checkpoint: 2 open, 1 need saving (Untitled). Save edited work at milestones and before ending the task. A source export or Document.CopyToDesktop is a separate checkpoint and may leave Fusion isSaved=false/isModified=true; verify the artifact and re-read save state. Close only this task's completed scratch documents by exact identity with fusion_close_document after verifying a current native checkpoint; never bulk-discard other work. If Fusion reports recovery-file failure, checkpoint immediately, then save or close that document; do not just dismiss recovery. Use a task subfolder when saving to cloud.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + }, + { + "isModified": true, + "isSaved": false, + "name": "Untitled", + "needsSave": true, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 1, + "openDocumentCount": 2, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1836, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0025-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0025-fusion_run_modeling_script.json new file mode 100644 index 0000000..64660f4 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0025-fusion_run_modeling_script.json @@ -0,0 +1,63 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"action\": \"close\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"action\\\": \\\"close\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235883.4982917, + "mutating": true, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"_timing\": {\n \"addinSec\": 0.41,\n \"dialogsSec\": 0.61,\n \"postSec\": 0.97,\n \"preSec\": 0.03,\n \"scope\": \"one exe pass-through of this verb only: excludes the relay hop, ab's queue, and any earlier pass-through a composite verb (routing, tour) made before this one, so the caller's wall clock can be longer\",\n \"totalSec\": 2.01\n },\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"closed\": true,\n \"closedDocumentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 2106,\n \"error\": \"\",\n \"output\": \"Workspace Deactivated event received\\nToolbar State: event: workspaceDeactivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\\nWorkspace Activated: Design\\nToolbar State: event: workspaceActivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235885.6115952, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "_timing": { + "addinSec": 0.41, + "dialogsSec": 0.61, + "postSec": 0.97, + "preSec": 0.03, + "scope": "one exe pass-through of this verb only: excludes the relay hop, ab's queue, and any earlier pass-through a composite verb (routing, tour) made before this one, so the caller's wall clock can be longer", + "totalSec": 2.01 + }, + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 2106, + "error": "", + "output": "Workspace Deactivated event received\nToolbar State: event: workspaceDeactivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType\nWorkspace Activated: Design\nToolbar State: event: workspaceActivated, tabId: AdomTab, isActive: False, isVisible: True, isValid: True, name: Adom, productType: DesignProductType", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0026-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0026-fusion_run_modeling_script.json new file mode 100644 index 0000000..a0b3532 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0026-fusion_run_modeling_script.json @@ -0,0 +1,81 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"action\": \"inventory\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"action\\\": \\\"inventory\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235885.6123796, + "mutating": false, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"documents\": [\n {\n \"checkpoint\": {\n \"bytes\": 65067,\n \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\"\n },\n \"closed\": true,\n \"creationId\": \"4f1af11b-0a58-4348-b81a-87502fd79fb2\",\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"name\": \"Untitled\",\n \"role\": \"01-scene_diagnostic\"\n },\n {\n \"checkpoint\": {\n \"bytes\": 65067,\n \"path\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d\",\n \"sha256\": \"239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764\"\n },\n \"closed\": true,\n \"creationId\": \"6f7c8295-af36-44c4-bbbc-fcdff5932a5e\",\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\",\n \"name\": \"Untitled\",\n \"role\": \"01-scene_diagnostic-reopened\"\n }\n ],\n \"provisionalImportCount\": 0,\n \"scratch\": \"C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1845,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235887.4657714, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "documents": [ + { + "checkpoint": { + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764" + }, + "closed": true, + "creationId": "4f1af11b-0a58-4348-b81a-87502fd79fb2", + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "name": "Untitled", + "role": "01-scene_diagnostic" + }, + { + "checkpoint": { + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764" + }, + "closed": true, + "creationId": "6f7c8295-af36-44c4-bbbc-fcdff5932a5e", + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "name": "Untitled", + "role": "01-scene_diagnostic-reopened" + } + ], + "provisionalImportCount": 0, + "scratch": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1845, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0027-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0027-fusion_run_modeling_script.json new file mode 100644 index 0000000..f02df51 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0027-fusion_run_modeling_script.json @@ -0,0 +1,56 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"action\": \"verify\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"action\\\": \\\"verify\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235887.4705055, + "mutating": false, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"closed\": true,\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"ownerRunId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1784,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235889.262218, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "closed": true, + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "ownerRunId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1784, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0028-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0028-fusion_run_modeling_script.json new file mode 100644 index 0000000..38fad2b --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0028-fusion_run_modeling_script.json @@ -0,0 +1,55 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\", \"action\": \"close\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\\\", \\\"action\\\": \\\"close\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235889.2630043, + "mutating": true, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"closed\": true,\n \"closedDocumentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1785,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235891.0571337, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1785, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0029-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0029-fusion_run_modeling_script.json new file mode 100644 index 0000000..83ac03c --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0029-fusion_run_modeling_script.json @@ -0,0 +1,56 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\", \"action\": \"verify\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\\\", \\\"action\\\": \\\"verify\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235891.0581348, + "mutating": false, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"closed\": true,\n \"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\",\n \"ownerRunId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1834,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235892.900767, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "closed": true, + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "ownerRunId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1834, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0030-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0030-fusion_run_modeling_script.json new file mode 100644 index 0000000..9201cee --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0030-fusion_run_modeling_script.json @@ -0,0 +1,55 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"documentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\", \"action\": \"close\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"documentId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\\\", \\\"action\\\": \\\"close\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235892.9016614, + "mutating": true, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"closed\": true,\n \"closedDocumentId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd\",\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1782,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235894.6925774, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1782, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0031-fusion_run_modeling_script.json b/docs/mechanical-verification/cp9-readonly/calls/0031-fusion_run_modeling_script.json new file mode 100644 index 0000000..3982a2a --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0031-fusion_run_modeling_script.json @@ -0,0 +1,62 @@ +{ + "target": "adom_drew2", + "verb": "fusion_run_modeling_script", + "args": { + "script": "import json\nREQUEST = json.loads('{\"action\": \"restore\", \"runId\": \"20261005T213050Z-422781656d64490da02934d1757322ed\", \"host\": \"ADOM_DREW2\", \"sessionId\": \"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\"}')\n\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\n\ndef require(condition, message):\n if not condition:\n raise RuntimeError(message)\n\ndef same(a, b):\n if isinstance(a, bool) or isinstance(b, bool):\n return type(a) is type(b) and a == b\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\n if isinstance(a, dict) and isinstance(b, dict):\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\n if isinstance(a, list) and isinstance(b, list):\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\n return a == b\n\ndef vector(p):\n return [p.x * 10, p.y * 10, p.z * 10]\n\ndef body_measure(body):\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n box = body.boundingBox\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\n 'material': body.material.name if body.material else None}\n\ndef witness(document):\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n require(design is not None, 'Owned document is no longer a design')\n root = design.rootComponent\n components, occurrences = [], []\n all_bodies = [body_measure(b) for b in root.bRepBodies]\n for comp in design.allComponents:\n components.append({'name': '' if comp == root else comp.name,\n 'partNumber': comp.partNumber, 'description': comp.description,\n 'material': comp.material.name if comp.material else None,\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\n for occurrence in root.allOccurrences:\n matrix = list(occurrence.transform2.asArray())\n for index in (3, 7, 11): matrix[index] *= 10\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\n all_bodies.extend(bodies)\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\n 'transform': matrix, 'bodies': bodies})\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\n return {'parameters': {p.name: p.expression for p in design.allParameters},\n 'timelineCount': design.timeline.count,\n 'components': sorted(components, key=lambda c: c['name']),\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\n 'centerOfMass': vector(props.centerOfMass)},\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\n 'instanceCount': len(all_bodies)}\n\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\nmodules = [m for n, m in list(sys.modules.items())\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\ncore = modules[0]\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\nif registries is None:\n registries = {}\n builtins._adom_mechanical_ralph_runs = registries\naction = REQUEST['action']\nrun = registries.get(REQUEST['runId'])\n\ndef documents():\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\n for d in app.documents]\n\ndef idle():\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\n\ndef fingerprint(document):\n return {'creationId': document.creationId, 'name': document.name,\n 'modified': document.isModified, 'saved': document.isSaved}\n\ndef owned(document_id, allow_closed=False):\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\n row = run['documents'][document_id]\n if row.get('closed'):\n require(allow_closed, 'Owned document is already closed')\n return row\n document = row['object']\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\n require(marker and marker.value == document_id, 'Runtime document marker changed')\n return row\n\ndef path_in_scratch(path):\n root = os.path.realpath(run['scratch'])\n path = os.path.abspath(path)\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\n 'Artifact is not inside the owned scratch directory')\n require(not os.path.islink(path), 'Artifact is a symlink')\n return path\n\ndef file_info(path):\n path = path_in_scratch(path)\n require(os.path.isfile(path), 'Checkpoint file is absent')\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\n with open(path, 'rb') as stream: data = stream.read()\n return {'path': path.replace('\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\n\ndef register(document, document_id, role):\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\n require(document_id not in run['documents'], 'Runtime ID is already registered')\n # Retain exact object first, including if marker creation later fails.\n row = {'object': document, 'documentId': document_id, 'role': role,\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\n run['documents'][document_id] = row\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\n return row\n\ndef public_row(row):\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\n\nif action == 'begin':\n idle()\n require(run is None, 'Native run registry already exists; use recover')\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\n baseline = documents()\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\n require(not os.path.exists(scratch), 'Run scratch already exists')\n parent = os.path.dirname(scratch)\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\n os.makedirs(scratch)\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\n 'baselineObjects': list(app.documents), 'baseline': baseline,\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\n registries[REQUEST['runId']] = run\n result = {'success': True, 'scratch': scratch.replace('\\\\', '/'), 'documents': baseline}\nelse:\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\n if action == 'inventory':\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\n 'scratch': run['scratch'].replace('\\\\', '/')}\n elif action == 'register':\n idle()\n document, design, document_id = core.active(app)\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\n row = register(document, document_id, REQUEST['role'])\n result = {'success': True, **public_row(row)}\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\n document = row['object']\n if action == 'verify':\n if not row.get('closed'):\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\n elif action == 'witness':\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\n elif action == 'activate':\n idle()\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing activation')\n if app.activeDocument != document:\n require(document.activate(), 'Owned document activation failed')\n result = {'success': True, 'documentId': row['documentId']}\n elif action == 'close':\n idle()\n if not row['closed']:\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\n 'User selected an external document; refusing lifecycle changes')\n checkpoint = row['checkpoint']\n require(checkpoint is not None, 'No verified native checkpoint')\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\n require(document.close(False), 'Owned document close returned false')\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\n row['closed'] = True\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\n elif action == 'capture':\n require(app.activeDocument == document, 'Active document changed before capture')\n path = path_in_scratch(REQUEST['path'])\n require(not os.path.exists(path), 'Capture destination already exists')\n viewport = app.activeViewport\n viewport.fit()\n viewport.refresh()\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\n result = {'success': True, **file_info(path)}\n else:\n idle()\n require(app.activeDocument == document, 'Active document changed before roundtrip')\n path = path_in_scratch(REQUEST['path'])\n archive = file_info(path)\n before = witness(document)\n options = app.importManager.createFusionArchiveImportOptions(path)\n imported = app.importManager.importToNewDocument(options)\n # Keep the actual returned API object before any fragile getter or\n # identity lookup. A provisional object can only be retained/reported.\n provisional = {'object': imported}\n run.setdefault('provisionalImports', []).append(provisional)\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\n reopened_doc, reopened_design, reopened_id = core.active(app)\n require(reopened_doc == imported, 'Imported document identity mismatch')\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\n run['provisionalImports'].remove(provisional)\n after = witness(imported)\n require(same(before, after), 'Native archive roundtrip changed measured model state')\n # Mark both sides only after independent semantic comparison.\n for item in (row, reopened):\n item['checkpoint'] = archive\n item['witness'] = before\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\n 'reopened': public_row(reopened), 'source': public_row(row)}\n elif action == 'file':\n info = file_info(REQUEST['path'])\n with open(info['path'], 'rb') as stream: data = stream.read()\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\n if REQUEST.get('csv'):\n with open(info['path'], encoding='utf-8', newline='') as stream:\n result['rows'] = list(csv.DictReader(stream))\n elif action == 'restore':\n idle()\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\n require(len(list(app.documents)) == len(run['baselineObjects']) and\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\n 'Document set changed; refusing to change active document')\n require(documents() == run['baseline'], 'Original document flags changed')\n original = run['activeObject']\n if app.activeDocument != original:\n require(original.activate(), 'Original document activation failed')\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\n else:\n raise RuntimeError('Unknown native action')\n" + }, + "requestJson": "{\"script\": \"import json\\nREQUEST = json.loads('{\\\"action\\\": \\\"restore\\\", \\\"runId\\\": \\\"20261005T213050Z-422781656d64490da02934d1757322ed\\\", \\\"host\\\": \\\"ADOM_DREW2\\\", \\\"sessionId\\\": \\\"dd15182e-3fa9-4ea3-bc57-4175e60ef00c\\\"}')\\n\\nimport base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile\\n\\ndef require(condition, message):\\n if not condition:\\n raise RuntimeError(message)\\n\\ndef same(a, b):\\n if isinstance(a, bool) or isinstance(b, bool):\\n return type(a) is type(b) and a == b\\n if isinstance(a, (int, float)) and isinstance(b, (int, float)):\\n return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7)\\n if isinstance(a, dict) and isinstance(b, dict):\\n return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a)\\n if isinstance(a, list) and isinstance(b, list):\\n return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b))\\n return a == b\\n\\ndef vector(p):\\n return [p.x * 10, p.y * 10, p.z * 10]\\n\\ndef body_measure(body):\\n properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n box = body.boundingBox\\n return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass,\\n 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count,\\n 'bounds': [vector(box.minPoint), vector(box.maxPoint)],\\n 'material': body.material.name if body.material else None}\\n\\ndef witness(document):\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n require(design is not None, 'Owned document is no longer a design')\\n root = design.rootComponent\\n components, occurrences = [], []\\n all_bodies = [body_measure(b) for b in root.bRepBodies]\\n for comp in design.allComponents:\\n components.append({'name': '' if comp == root else comp.name,\\n 'partNumber': comp.partNumber, 'description': comp.description,\\n 'material': comp.material.name if comp.material else None,\\n 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'},\\n 'bodies': [body_measure(b) for b in comp.bRepBodies],\\n 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained,\\n 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]})\\n for occurrence in root.allOccurrences:\\n matrix = list(occurrence.transform2.asArray())\\n for index in (3, 7, 11): matrix[index] *= 10\\n bodies = [body_measure(b) for b in occurrence.bRepBodies]\\n all_bodies.extend(bodies)\\n occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name,\\n 'transform': matrix, 'bodies': bodies})\\n props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy)\\n return {'parameters': {p.name: p.expression for p in design.allParameters},\\n 'timelineCount': design.timeline.count,\\n 'components': sorted(components, key=lambda c: c['name']),\\n 'occurrences': sorted(occurrences, key=lambda o: o['path']),\\n 'physical': {'volume': props.volume * 1000, 'mass': props.mass,\\n 'centerOfMass': vector(props.centerOfMass)},\\n 'instanceVolume': sum(b['volume'] for b in all_bodies),\\n 'instanceCount': len(all_bodies)}\\n\\nrequire(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host')\\nrequire(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID')\\nmodules = [m for n, m in list(sys.modules.items())\\n if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']]\\nrequire(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference')\\ncore = modules[0]\\nregistries = getattr(builtins, '_adom_mechanical_ralph_runs', None)\\nif registries is None:\\n registries = {}\\n builtins._adom_mechanical_ralph_runs = registries\\naction = REQUEST['action']\\nrun = registries.get(REQUEST['runId'])\\n\\ndef documents():\\n return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved}\\n for d in app.documents]\\n\\ndef idle():\\n require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action')\\n\\ndef fingerprint(document):\\n return {'creationId': document.creationId, 'name': document.name,\\n 'modified': document.isModified, 'saved': document.isSaved}\\n\\ndef owned(document_id, allow_closed=False):\\n require(document_id in run['documents'], 'Document has no exact native ownership entry')\\n row = run['documents'][document_id]\\n if row.get('closed'):\\n require(allow_closed, 'Owned document is already closed')\\n return row\\n document = row['object']\\n require(any(d == document for d in app.documents), 'Exact owned document is no longer open')\\n require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document')\\n require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner')\\n require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed')\\n marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId')\\n require(marker and marker.value == document_id, 'Runtime document marker changed')\\n return row\\n\\ndef path_in_scratch(path):\\n root = os.path.realpath(run['scratch'])\\n path = os.path.abspath(path)\\n require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root,\\n 'Artifact is not inside the owned scratch directory')\\n require(not os.path.islink(path), 'Artifact is a symlink')\\n return path\\n\\ndef file_info(path):\\n path = path_in_scratch(path)\\n require(os.path.isfile(path), 'Checkpoint file is absent')\\n require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget')\\n with open(path, 'rb') as stream: data = stream.read()\\n return {'path': path.replace('\\\\\\\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)}\\n\\ndef register(document, document_id, role):\\n require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document')\\n require(document_id not in run['documents'], 'Runtime ID is already registered')\\n # Retain exact object first, including if marker creation later fails.\\n row = {'object': document, 'documentId': document_id, 'role': role,\\n 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None}\\n run['documents'][document_id] = row\\n require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned')\\n design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType'))\\n design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId'])\\n design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id)\\n return row\\n\\ndef public_row(row):\\n return {k: v for k, v in row.items() if k not in ('object', 'witness')}\\n\\nif action == 'begin':\\n idle()\\n require(run is None, 'Native run registry already exists; use recover')\\n require(app.documents.count > 0, 'An existing active design is required for a read-only baseline')\\n baseline = documents()\\n require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start')\\n scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId'])\\n require(not os.path.exists(scratch), 'Run scratch already exists')\\n parent = os.path.dirname(scratch)\\n require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link')\\n os.makedirs(scratch)\\n run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch,\\n 'baselineObjects': list(app.documents), 'baseline': baseline,\\n 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)}\\n registries[REQUEST['runId']] = run\\n result = {'success': True, 'scratch': scratch.replace('\\\\\\\\', '/'), 'documents': baseline}\\nelse:\\n require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted')\\n if action == 'inventory':\\n result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()],\\n 'provisionalImportCount': len(run.get('provisionalImports', [])),\\n 'scratch': run['scratch'].replace('\\\\\\\\', '/')}\\n elif action == 'register':\\n idle()\\n document, design, document_id = core.active(app)\\n require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result')\\n row = register(document, document_id, REQUEST['role'])\\n result = {'success': True, **public_row(row)}\\n elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'):\\n row = owned(REQUEST['documentId'], action in ('close', 'verify'))\\n document = row['object']\\n if action == 'verify':\\n if not row.get('closed'):\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']}\\n elif action == 'witness':\\n require(app.activeDocument == document, 'Active document changed; no implicit activation')\\n result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)}\\n elif action == 'activate':\\n idle()\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing activation')\\n if app.activeDocument != document:\\n require(document.activate(), 'Owned document activation failed')\\n result = {'success': True, 'documentId': row['documentId']}\\n elif action == 'close':\\n idle()\\n if not row['closed']:\\n require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']),\\n 'User selected an external document; refusing lifecycle changes')\\n checkpoint = row['checkpoint']\\n require(checkpoint is not None, 'No verified native checkpoint')\\n require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed')\\n require(same(witness(document), row['witness']), 'Model changed since checkpoint')\\n require(document.close(False), 'Owned document close returned false')\\n require(not any(d == document for d in app.documents), 'Owned document still appears in open documents')\\n row['closed'] = True\\n result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True}\\n elif action == 'capture':\\n require(app.activeDocument == document, 'Active document changed before capture')\\n path = path_in_scratch(REQUEST['path'])\\n require(not os.path.exists(path), 'Capture destination already exists')\\n viewport = app.activeViewport\\n viewport.fit()\\n viewport.refresh()\\n require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed')\\n result = {'success': True, **file_info(path)}\\n else:\\n idle()\\n require(app.activeDocument == document, 'Active document changed before roundtrip')\\n path = path_in_scratch(REQUEST['path'])\\n archive = file_info(path)\\n before = witness(document)\\n options = app.importManager.createFusionArchiveImportOptions(path)\\n imported = app.importManager.importToNewDocument(options)\\n # Keep the actual returned API object before any fragile getter or\\n # identity lookup. A provisional object can only be retained/reported.\\n provisional = {'object': imported}\\n run.setdefault('provisionalImports', []).append(provisional)\\n require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document')\\n reopened_doc, reopened_design, reopened_id = core.active(app)\\n require(reopened_doc == imported, 'Imported document identity mismatch')\\n reopened = register(imported, reopened_id, row['role'] + '-reopened')\\n run['provisionalImports'].remove(provisional)\\n after = witness(imported)\\n require(same(before, after), 'Native archive roundtrip changed measured model state')\\n # Mark both sides only after independent semantic comparison.\\n for item in (row, reopened):\\n item['checkpoint'] = archive\\n item['witness'] = before\\n result = {'success': True, 'archive': archive, 'before': before, 'after': after,\\n 'reopened': public_row(reopened), 'source': public_row(row)}\\n elif action == 'file':\\n info = file_info(REQUEST['path'])\\n with open(info['path'], 'rb') as stream: data = stream.read()\\n result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')}\\n if REQUEST.get('csv'):\\n with open(info['path'], encoding='utf-8', newline='') as stream:\\n result['rows'] = list(csv.DictReader(stream))\\n elif action == 'restore':\\n idle()\\n require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused')\\n require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open')\\n require(len(list(app.documents)) == len(run['baselineObjects']) and\\n all(any(d == original for d in app.documents) for original in run['baselineObjects']),\\n 'Document set changed; refusing to change active document')\\n require(documents() == run['baseline'], 'Original document flags changed')\\n original = run['activeObject']\\n if app.activeDocument != original:\\n require(original.activate(), 'Original document activation failed')\\n require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed')\\n result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId}\\n else:\\n raise RuntimeError('Unknown native action')\\n\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235894.6949039, + "mutating": true, + "stdout": "{\n \"_hint\": \"Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\\\"foreground\\\": true, \\\"foregroundReason\\\": \\\"\\\"}.\",\n \"_timeoutHint\": \"ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"data\": {\n \"createdDocument\": null,\n \"documentName\": \"Untitled\",\n \"result\": {\n \"activeCreationId\": \"cdf1e4ec-a727-4ffe-8975-d10e11191864\",\n \"documents\": [\n {\n \"creationId\": \"cdf1e4ec-a727-4ffe-8975-d10e11191864\",\n \"modified\": false,\n \"name\": \"Untitled\",\n \"saved\": false\n }\n ],\n \"success\": true\n }\n },\n \"documentHygiene\": {\n \"documents\": [\n {\n \"isModified\": false,\n \"isSaved\": false,\n \"name\": \"Untitled\",\n \"needsSave\": false,\n \"saveStateKnown\": true\n }\n ],\n \"inspectionComplete\": true,\n \"needsSaveCount\": 0,\n \"openDocumentCount\": 1,\n \"ownership\": \"notInferred\",\n \"recoveryHealth\": \"notInspected\"\n },\n \"durationMs\": 1799,\n \"error\": \"\",\n \"output\": \"Modeling script ran successfully.\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 180,\n \"windowEtiquette\": \"background\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235896.5032818, + "raw": { + "_hint": "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"\"}.", + "_timeoutHint": "ab waits up to 180s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "data": { + "createdDocument": null, + "documentName": "Untitled", + "result": { + "activeCreationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "documents": [ + { + "creationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "modified": false, + "name": "Untitled", + "saved": false + } + ], + "success": true + } + }, + "documentHygiene": { + "documents": [ + { + "isModified": false, + "isSaved": false, + "name": "Untitled", + "needsSave": false, + "saveStateKnown": true + } + ], + "inspectionComplete": true, + "needsSaveCount": 0, + "openDocumentCount": 1, + "ownership": "notInferred", + "recoveryHealth": "notInspected" + }, + "durationMs": 1799, + "error": "", + "output": "Modeling script ran successfully.", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 180, + "windowEtiquette": "background" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0032-fusion_get_preferences.json b/docs/mechanical-verification/cp9-readonly/calls/0032-fusion_get_preferences.json new file mode 100644 index 0000000..448c1d4 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0032-fusion_get_preferences.json @@ -0,0 +1,38 @@ +{ + "target": "adom_drew2", + "verb": "fusion_get_preferences", + "args": {}, + "requestJson": "{}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235896.5040593, + "mutating": false, + "stdout": "{\n \"_hint\": \"Current Fusion preferences. Change any with fusion_set_preference, keys: theme (light/darkblue/darkgray/classic/device/dark), invertScrollZoom (bool), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - applies to the active design).\",\n \"_timeoutHint\": \"ab waits up to 25s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"applied\": {},\n \"current\": {\n \"activeTheme\": \"darkblue\",\n \"cameraPivot\": true,\n \"gestureNav\": true,\n \"invertScrollZoom\": true,\n \"lengthUnit\": \"mm\",\n \"modelingOrientation\": \"zup\",\n \"orbitScheme\": \"fusion360\",\n \"theme\": \"device\"\n },\n \"durationMs\": 142,\n \"error\": \"\",\n \"output\": \"{\\\"_hint\\\":\\\"Current Fusion preferences. Change any with fusion_set_preference, keys: theme (light/darkblue/darkgray/classic/device/dark), invertScrollZoom (bool), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - applies to the active design).\\\",\\\"_timeoutHint\\\":\\\"ab waits up to 25s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\\\",\\\"applied\\\":{},\\\"current\\\":{\\\"activeTheme\\\":\\\"darkblue\\\",\\\"cameraPivot\\\":true,\\\"gestureNav\\\":true,\\\"invertScrollZoom\\\":true,\\\"lengthUnit\\\":\\\"mm\\\",\\\"modelingOrientation\\\":\\\"zup\\\",\\\"orbitScheme\\\":\\\"fusion360\\\",\\\"theme\\\":\\\"device\\\"},\\\"shell\\\":\\\"rust\\\",\\\"statusVerb\\\":\\\"fusion_get_app_state\\\",\\\"success\\\":true,\\\"timeoutSeconds\\\":25}\",\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 25\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235896.655026, + "raw": { + "_hint": "Current Fusion preferences. Change any with fusion_set_preference, keys: theme (light/darkblue/darkgray/classic/device/dark), invertScrollZoom (bool), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - applies to the active design).", + "_timeoutHint": "ab waits up to 25s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "applied": {}, + "current": { + "activeTheme": "darkblue", + "cameraPivot": true, + "gestureNav": true, + "invertScrollZoom": true, + "lengthUnit": "mm", + "modelingOrientation": "zup", + "orbitScheme": "fusion360", + "theme": "device" + }, + "durationMs": 142, + "error": "", + "output": "{\"_hint\":\"Current Fusion preferences. Change any with fusion_set_preference, keys: theme (light/darkblue/darkgray/classic/device/dark), invertScrollZoom (bool), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - applies to the active design).\",\"_timeoutHint\":\"ab waits up to 25s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\"applied\":{},\"current\":{\"activeTheme\":\"darkblue\",\"cameraPivot\":true,\"gestureNav\":true,\"invertScrollZoom\":true,\"lengthUnit\":\"mm\",\"modelingOrientation\":\"zup\",\"orbitScheme\":\"fusion360\",\"theme\":\"device\"},\"shell\":\"rust\",\"statusVerb\":\"fusion_get_app_state\",\"success\":true,\"timeoutSeconds\":25}", + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 25 + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0033-fusion_state.json b/docs/mechanical-verification/cp9-readonly/calls/0033-fusion_state.json new file mode 100644 index 0000000..c970b40 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0033-fusion_state.json @@ -0,0 +1,439 @@ +{ + "target": "adom_drew2", + "verb": "fusion_state", + "args": { + "screenshot": false, + "nativeViewport": false + }, + "requestJson": "{\"screenshot\": false, \"nativeViewport\": false}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235896.6557586, + "mutating": false, + "stdout": "{\n \"_hint\": \"Fusion is clear. The screenshot shows the current surface - this is the verb to render any fusion_show_* result (they open in the background by design).\",\n \"_timeoutHint\": \"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"activeDocument\": \"Untitled\",\n \"addinResponding\": true,\n \"blockingDialogs\": [],\n \"dialogs\": [],\n \"drivingBadge\": {\n \"active\": true,\n \"enabled\": true,\n \"idleShedSeconds\": 300,\n \"lastDrivingVerbAt\": 1791235894.8066213,\n \"lastError\": null\n },\n \"durationMs\": 145,\n \"error\": \"\",\n \"focusEvents\": [],\n \"hwnd\": 4658928,\n \"mainThreadBusy\": null,\n \"output\": \"{\\\"running\\\":true,\\\"activeDocument\\\":\\\"Untitled\\\",\\\"mainThreadBusy\\\":null,\\\"blockingDialogs\\\":[],\\\"screenshot\\\":null}\",\n \"rect\": {\n \"bottom\": 1549,\n \"height\": 1549,\n \"left\": -11,\n \"right\": 2572,\n \"top\": 0,\n \"width\": 2583\n },\n \"running\": true,\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 60,\n \"timings\": {\n \"launch\": {\n \"cold\": {\n \"count\": 10,\n \"lastAt\": 1791234147,\n \"p50\": 41.1,\n \"p90\": 216.2\n },\n \"warm\": {\n \"count\": 2,\n \"lastAt\": 1791230617,\n \"p50\": 0.0,\n \"p90\": 1.0\n }\n },\n \"verb:activate_document\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1789145672,\n \"p50\": 3.1,\n \"p90\": 5.6\n }\n },\n \"verb:addin_status\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1788899767,\n \"p50\": 2.0,\n \"p90\": 2.0\n }\n },\n \"verb:aps_browse\": {\n \"any\": {\n \"count\": 4,\n \"lastAt\": 1789143968,\n \"p50\": 1.9,\n \"p90\": 2.3\n }\n },\n \"verb:aps_file_info\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1789144745,\n \"p50\": 34.8,\n \"p90\": 36.2\n }\n },\n \"verb:aps_get_browser\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1787944552,\n \"p50\": 0.0,\n \"p90\": 0.0\n }\n },\n \"verb:aps_open\": {\n \"any\": {\n \"count\": 5,\n \"lastAt\": 1788196241,\n \"p50\": 0.2,\n \"p90\": 37.2\n }\n },\n \"verb:aps_search\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1789684900,\n \"p50\": 35.7,\n \"p90\": 65.6\n }\n },\n \"verb:aps_signin\": {\n \"any\": {\n \"count\": 2,\n \"lastAt\": 1787949063,\n \"p50\": 0.4,\n \"p90\": 1.1\n }\n },\n \"verb:aps_status\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1788899782,\n \"p50\": 0.0,\n \"p90\": 0.0\n }\n },\n \"verb:assembly_bom\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1790955710,\n \"p50\": 0.1,\n \"p90\": 0.6\n }\n },\n \"verb:check_dialogs\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1788195388,\n \"p50\": 1.4,\n \"p90\": 1.4\n }\n },\n \"verb:close_document\": {\n \"any\": {\n \"count\": 9,\n \"lastAt\": 1789145809,\n \"p50\": 6.7,\n \"p90\": 46.1\n }\n },\n \"verb:describe\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1789144008,\n \"p50\": 0.0,\n \"p90\": 0.0\n }\n },\n \"verb:document_info\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791235856,\n \"p50\": 0.1,\n \"p90\": 0.2\n }\n },\n \"verb:eagle_import_status\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1790978404,\n \"p50\": 1.8,\n \"p90\": 1.8\n }\n },\n \"verb:electron_run\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1788456727,\n \"p50\": 6.3,\n \"p90\": 13.3\n }\n },\n \"verb:export_cloud_file\": {\n \"any\": {\n \"count\": 4,\n \"lastAt\": 1789685179,\n \"p50\": 223.4,\n \"p90\": 290.3\n }\n },\n \"verb:export_eagle_source\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1788456713,\n \"p50\": 1.7,\n \"p90\": 2.2\n }\n },\n \"verb:export_f3d\": {\n \"any\": {\n \"count\": 2,\n \"lastAt\": 1790955227,\n \"p50\": 3.7,\n \"p90\": 4.4\n }\n },\n \"verb:export_obj\": {\n \"any\": {\n \"count\": 2,\n \"lastAt\": 1787944913,\n \"p50\": 0.7,\n \"p90\": 0.7\n }\n },\n \"verb:export_step\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1788456783,\n \"p50\": 5.5,\n \"p90\": 7.9\n }\n },\n \"verb:get_app_state\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791234147,\n \"p50\": 1.3,\n \"p90\": 3.6\n }\n },\n \"verb:get_design_info\": {\n \"any\": {\n \"count\": 5,\n \"lastAt\": 1788900070,\n \"p50\": 5.2,\n \"p90\": 10.8\n }\n },\n \"verb:help\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1788194522,\n \"p50\": 1.0,\n \"p90\": 1.0\n }\n },\n \"verb:kill\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1787833443,\n \"p50\": 66.9,\n \"p90\": 66.9\n }\n },\n \"verb:list_cloud_files\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1787832975,\n \"p50\": 15.8,\n \"p90\": 15.8\n }\n },\n \"verb:mcp_call\": {\n \"any\": {\n \"count\": 5,\n \"lastAt\": 1788455586,\n \"p50\": 0.0,\n \"p90\": 1.8\n }\n },\n \"verb:mech_capabilities\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791235858,\n \"p50\": 0.1,\n \"p90\": 0.1\n }\n },\n \"verb:mech_operation_status\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791229196,\n \"p50\": 0.1,\n \"p90\": 0.1\n }\n },\n \"verb:mechanical\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791235875,\n \"p50\": 0.8,\n \"p90\": 1.3\n }\n },\n \"verb:open_by_urn\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1789144985,\n \"p50\": 43.4,\n \"p90\": 59.1\n }\n },\n \"verb:open_cloud_file\": {\n \"any\": {\n \"count\": 8,\n \"lastAt\": 1789666231,\n \"p50\": 24.2,\n \"p90\": 87.5\n }\n },\n \"verb:open_design\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1790955721,\n \"p50\": 5.9,\n \"p90\": 6.1\n }\n },\n \"verb:open_lbr\": {\n \"any\": {\n \"count\": 2,\n \"lastAt\": 1788195396,\n \"p50\": 2.7,\n \"p90\": 5.0\n }\n },\n \"verb:physical_properties\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1790955710,\n \"p50\": 0.1,\n \"p90\": 0.2\n }\n },\n \"verb:readiness\": {\n \"any\": {\n \"count\": 5,\n \"lastAt\": 1788900108,\n \"p50\": 14.9,\n \"p90\": 56.9\n }\n },\n \"verb:restart_for_addin\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791234174,\n \"p50\": 0.0,\n \"p90\": 125.2\n }\n },\n \"verb:run_modeling_script\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1791235896,\n \"p50\": 1.8,\n \"p90\": 2.0\n }\n },\n \"verb:search_cloud_files\": {\n \"any\": {\n \"count\": 3,\n \"lastAt\": 1789143642,\n \"p50\": 0.0,\n \"p90\": 0.4\n }\n },\n \"verb:show_2d_board\": {\n \"any\": {\n \"count\": 2,\n \"lastAt\": 1788456656,\n \"p50\": 26.6,\n \"p90\": 34.9\n }\n },\n \"verb:show_3d_board\": {\n \"any\": {\n \"count\": 6,\n \"lastAt\": 1788456760,\n \"p50\": 10.2,\n \"p90\": 19.6\n }\n },\n \"verb:signin\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1788899909,\n \"p50\": 56.1,\n \"p90\": 56.1\n }\n },\n \"verb:start\": {\n \"any\": {\n \"count\": 7,\n \"lastAt\": 1791230617,\n \"p50\": 57.4,\n \"p90\": 64.9\n }\n },\n \"verb:status\": {\n \"any\": {\n \"count\": 2,\n \"lastAt\": 1788194217,\n \"p50\": 0.8,\n \"p90\": 1.0\n }\n },\n \"verb:stop\": {\n \"any\": {\n \"count\": 1,\n \"lastAt\": 1787831926,\n \"p50\": 19.7,\n \"p90\": 19.7\n }\n },\n \"verb:window_info\": {\n \"any\": {\n \"count\": 10,\n \"lastAt\": 1788899841,\n \"p50\": 7.2,\n \"p90\": 10.9\n }\n }\n },\n \"timingsSentence\": \"on this machine Fusion starts in cold 41s, warm 0s (measured)\",\n \"title\": \"Untitled - Autodesk Fusion \"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235896.8090866, + "raw": { + "_hint": "Fusion is clear. The screenshot shows the current surface - this is the verb to render any fusion_show_* result (they open in the background by design).", + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "activeDocument": "Untitled", + "addinResponding": true, + "blockingDialogs": [], + "dialogs": [], + "drivingBadge": { + "active": true, + "enabled": true, + "idleShedSeconds": 300, + "lastDrivingVerbAt": 1791235894.8066213, + "lastError": null + }, + "durationMs": 145, + "error": "", + "focusEvents": [], + "hwnd": 4658928, + "mainThreadBusy": null, + "output": "{\"running\":true,\"activeDocument\":\"Untitled\",\"mainThreadBusy\":null,\"blockingDialogs\":[],\"screenshot\":null}", + "rect": { + "bottom": 1549, + "height": 1549, + "left": -11, + "right": 2572, + "top": 0, + "width": 2583 + }, + "running": true, + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60, + "timings": { + "launch": { + "cold": { + "count": 10, + "lastAt": 1791234147, + "p50": 41.1, + "p90": 216.2 + }, + "warm": { + "count": 2, + "lastAt": 1791230617, + "p50": 0.0, + "p90": 1.0 + } + }, + "verb:activate_document": { + "any": { + "count": 3, + "lastAt": 1789145672, + "p50": 3.1, + "p90": 5.6 + } + }, + "verb:addin_status": { + "any": { + "count": 3, + "lastAt": 1788899767, + "p50": 2.0, + "p90": 2.0 + } + }, + "verb:aps_browse": { + "any": { + "count": 4, + "lastAt": 1789143968, + "p50": 1.9, + "p90": 2.3 + } + }, + "verb:aps_file_info": { + "any": { + "count": 10, + "lastAt": 1789144745, + "p50": 34.8, + "p90": 36.2 + } + }, + "verb:aps_get_browser": { + "any": { + "count": 1, + "lastAt": 1787944552, + "p50": 0.0, + "p90": 0.0 + } + }, + "verb:aps_open": { + "any": { + "count": 5, + "lastAt": 1788196241, + "p50": 0.2, + "p90": 37.2 + } + }, + "verb:aps_search": { + "any": { + "count": 10, + "lastAt": 1789684900, + "p50": 35.7, + "p90": 65.6 + } + }, + "verb:aps_signin": { + "any": { + "count": 2, + "lastAt": 1787949063, + "p50": 0.4, + "p90": 1.1 + } + }, + "verb:aps_status": { + "any": { + "count": 10, + "lastAt": 1788899782, + "p50": 0.0, + "p90": 0.0 + } + }, + "verb:assembly_bom": { + "any": { + "count": 3, + "lastAt": 1790955710, + "p50": 0.1, + "p90": 0.6 + } + }, + "verb:check_dialogs": { + "any": { + "count": 1, + "lastAt": 1788195388, + "p50": 1.4, + "p90": 1.4 + } + }, + "verb:close_document": { + "any": { + "count": 9, + "lastAt": 1789145809, + "p50": 6.7, + "p90": 46.1 + } + }, + "verb:describe": { + "any": { + "count": 3, + "lastAt": 1789144008, + "p50": 0.0, + "p90": 0.0 + } + }, + "verb:document_info": { + "any": { + "count": 10, + "lastAt": 1791235856, + "p50": 0.1, + "p90": 0.2 + } + }, + "verb:eagle_import_status": { + "any": { + "count": 1, + "lastAt": 1790978404, + "p50": 1.8, + "p90": 1.8 + } + }, + "verb:electron_run": { + "any": { + "count": 10, + "lastAt": 1788456727, + "p50": 6.3, + "p90": 13.3 + } + }, + "verb:export_cloud_file": { + "any": { + "count": 4, + "lastAt": 1789685179, + "p50": 223.4, + "p90": 290.3 + } + }, + "verb:export_eagle_source": { + "any": { + "count": 10, + "lastAt": 1788456713, + "p50": 1.7, + "p90": 2.2 + } + }, + "verb:export_f3d": { + "any": { + "count": 2, + "lastAt": 1790955227, + "p50": 3.7, + "p90": 4.4 + } + }, + "verb:export_obj": { + "any": { + "count": 2, + "lastAt": 1787944913, + "p50": 0.7, + "p90": 0.7 + } + }, + "verb:export_step": { + "any": { + "count": 10, + "lastAt": 1788456783, + "p50": 5.5, + "p90": 7.9 + } + }, + "verb:get_app_state": { + "any": { + "count": 10, + "lastAt": 1791234147, + "p50": 1.3, + "p90": 3.6 + } + }, + "verb:get_design_info": { + "any": { + "count": 5, + "lastAt": 1788900070, + "p50": 5.2, + "p90": 10.8 + } + }, + "verb:help": { + "any": { + "count": 1, + "lastAt": 1788194522, + "p50": 1.0, + "p90": 1.0 + } + }, + "verb:kill": { + "any": { + "count": 1, + "lastAt": 1787833443, + "p50": 66.9, + "p90": 66.9 + } + }, + "verb:list_cloud_files": { + "any": { + "count": 1, + "lastAt": 1787832975, + "p50": 15.8, + "p90": 15.8 + } + }, + "verb:mcp_call": { + "any": { + "count": 5, + "lastAt": 1788455586, + "p50": 0.0, + "p90": 1.8 + } + }, + "verb:mech_capabilities": { + "any": { + "count": 10, + "lastAt": 1791235858, + "p50": 0.1, + "p90": 0.1 + } + }, + "verb:mech_operation_status": { + "any": { + "count": 10, + "lastAt": 1791229196, + "p50": 0.1, + "p90": 0.1 + } + }, + "verb:mechanical": { + "any": { + "count": 10, + "lastAt": 1791235875, + "p50": 0.8, + "p90": 1.3 + } + }, + "verb:open_by_urn": { + "any": { + "count": 10, + "lastAt": 1789144985, + "p50": 43.4, + "p90": 59.1 + } + }, + "verb:open_cloud_file": { + "any": { + "count": 8, + "lastAt": 1789666231, + "p50": 24.2, + "p90": 87.5 + } + }, + "verb:open_design": { + "any": { + "count": 3, + "lastAt": 1790955721, + "p50": 5.9, + "p90": 6.1 + } + }, + "verb:open_lbr": { + "any": { + "count": 2, + "lastAt": 1788195396, + "p50": 2.7, + "p90": 5.0 + } + }, + "verb:physical_properties": { + "any": { + "count": 3, + "lastAt": 1790955710, + "p50": 0.1, + "p90": 0.2 + } + }, + "verb:readiness": { + "any": { + "count": 5, + "lastAt": 1788900108, + "p50": 14.9, + "p90": 56.9 + } + }, + "verb:restart_for_addin": { + "any": { + "count": 10, + "lastAt": 1791234174, + "p50": 0.0, + "p90": 125.2 + } + }, + "verb:run_modeling_script": { + "any": { + "count": 10, + "lastAt": 1791235896, + "p50": 1.8, + "p90": 2.0 + } + }, + "verb:search_cloud_files": { + "any": { + "count": 3, + "lastAt": 1789143642, + "p50": 0.0, + "p90": 0.4 + } + }, + "verb:show_2d_board": { + "any": { + "count": 2, + "lastAt": 1788456656, + "p50": 26.6, + "p90": 34.9 + } + }, + "verb:show_3d_board": { + "any": { + "count": 6, + "lastAt": 1788456760, + "p50": 10.2, + "p90": 19.6 + } + }, + "verb:signin": { + "any": { + "count": 1, + "lastAt": 1788899909, + "p50": 56.1, + "p90": 56.1 + } + }, + "verb:start": { + "any": { + "count": 7, + "lastAt": 1791230617, + "p50": 57.4, + "p90": 64.9 + } + }, + "verb:status": { + "any": { + "count": 2, + "lastAt": 1788194217, + "p50": 0.8, + "p90": 1.0 + } + }, + "verb:stop": { + "any": { + "count": 1, + "lastAt": 1787831926, + "p50": 19.7, + "p90": 19.7 + } + }, + "verb:window_info": { + "any": { + "count": 10, + "lastAt": 1788899841, + "p50": 7.2, + "p90": 10.9 + } + } + }, + "timingsSentence": "on this machine Fusion starts in cold 41s, warm 0s (measured)", + "title": "Untitled - Autodesk Fusion " + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0034-fusion_addin_status.json b/docs/mechanical-verification/cp9-readonly/calls/0034-fusion_addin_status.json new file mode 100644 index 0000000..6704388 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0034-fusion_addin_status.json @@ -0,0 +1,32 @@ +{ + "target": "adom_drew2", + "verb": "fusion_addin_status", + "args": {}, + "requestJson": "{}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235896.810337, + "mutating": false, + "stdout": "{\n \"_timeoutHint\": \"ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.\",\n \"addinStale\": false,\n \"addinVersion\": \"1.12.0-dev.8\",\n \"busy\": false,\n \"durationMs\": 76,\n \"error\": \"\",\n \"expectedAddinVersion\": \"1.12.0-dev.8\",\n \"output\": \"{\\\"busy\\\":false,\\\"version\\\":\\\"1.12.0-dev.8\\\",\\\"pyThreads\\\":4,\\\"addinVersion\\\":\\\"1.12.0-dev.8\\\",\\\"expectedAddinVersion\\\":\\\"1.12.0-dev.8\\\",\\\"addinStale\\\":false}\",\n \"pyThreads\": 4,\n \"schemaVersion\": 1,\n \"shell\": \"rust\",\n \"status\": \"ok\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"success\": true,\n \"timeoutSeconds\": 60,\n \"version\": \"1.12.0-dev.8\"\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235896.8937812, + "raw": { + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "addinStale": false, + "addinVersion": "1.12.0-dev.8", + "busy": false, + "durationMs": 76, + "error": "", + "expectedAddinVersion": "1.12.0-dev.8", + "output": "{\"busy\":false,\"version\":\"1.12.0-dev.8\",\"pyThreads\":4,\"addinVersion\":\"1.12.0-dev.8\",\"expectedAddinVersion\":\"1.12.0-dev.8\",\"addinStale\":false}", + "pyThreads": 4, + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60, + "version": "1.12.0-dev.8" + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0035-bridge_list.json b/docs/mechanical-verification/cp9-readonly/calls/0035-bridge_list.json new file mode 100644 index 0000000..129cff1 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0035-bridge_list.json @@ -0,0 +1,1455 @@ +{ + "target": "adom_drew2", + "verb": "bridge_list", + "args": {}, + "requestJson": "{}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235896.8943064, + "mutating": false, + "stdout": "{\n \"_hint\": \"Per-bridge runtime info. `source` tells you where the bridge code lives: bundled (installer ships it) / cache (wiki-streamed, writable) / third_party (installed via bridge_install). Use bridge_install to add a new bridge from a wiki manifest URL, bridge_pause/resume to control lifecycle without uninstalling, bridge_uninstall to remove. v1.8.0+.\",\n \"_skillsHint\": \"For each bridge here with skillPkg != null, the container should run its `installSkill` to get that bridge's SKILL.md into ~/.claude/skills/ (then `adom-wiki pkg update` keeps them current). skillPkg:null bridges ship their skill in the adom-bridge core pkg (or have none).\",\n \"bridges\": [\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"CAD\",\n \"depsInstallingSec\": null,\n \"description\": \"Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.\",\n \"devPinned\": true,\n \"displayName\": \"Autodesk Fusion 360\",\n \"docs\": \"https://wiki.adom.inc/adom/fusion-bridge\",\n \"entryMismatch\": false,\n \"entrypoint\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/fusion360/adom-fusion-bridge.exe\",\n \"expectedImage\": \"adom-fusion-bridge.exe\",\n \"heroAvailable\": false,\n \"heroDocs\": null,\n \"homepage\": \"https://wiki.adom.inc/adom/fusion-bridge\",\n \"installPath\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/fusion360\",\n \"installSkill\": \"adom-wiki pkg install adom/fusion-bridge\",\n \"installedVersion\": \"1.12.0-dev.8\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": 1,\n \"languages\": [\n \"Python\"\n ],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 1791235896837,\n \"latest\": null,\n \"livePids\": [\n 42780\n ],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/fusion360.log\",\n \"manifestSource\": null,\n \"manifestUrl\": \"https://wiki.adom.inc/api/v1/pages/fusion-bridge/files/adom-bridge-fusion-manifest.json\",\n \"manifestUrlFollowed\": null,\n \"name\": \"fusion360\",\n \"paused\": false,\n \"persistent\": true,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"Autodesk doesn't ship Fusion 360 for Linux, so there's no host app for the add-in to attach to.\",\n \"supported\": false\n },\n \"macos\": {\n \"reason\": \"Fusion 360 runs on macOS; bridge + add-in install paths handled by fusion_detect.py.\",\n \"supported\": true\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": 50618,\n \"prewarm\": {\n \"assets\": [\n \"AdomBridge add-in sync\",\n \"PCB design-rules catalog cache\"\n ],\n \"lastRun\": {\n \"at\": \"2026-10-05T15:41:57Z\",\n \"elapsedMs\": 2344,\n \"error\": null,\n \"ok\": true,\n \"verb\": \"fusion_prewarm\",\n \"version\": \"1.12.0-dev.5\"\n },\n \"requiresHostApp\": true,\n \"timeoutSeconds\": null,\n \"verb\": \"fusion_prewarm\"\n },\n \"releasedAt\": \"2026-06-26T06:00:00Z\",\n \"runningImage\": \"adom-fusion-bridge.exe\",\n \"runningVersion\": \"1.12.0-dev.5\",\n \"skillPkg\": \"adom/fusion-bridge\",\n \"source\": \"cache\",\n \"sourceDir\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/fusion360\",\n \"spawn\": {\n \"entrypoint\": \"adom-fusion-bridge.exe\",\n \"instanceCount\": 1,\n \"kind\": \"exe\",\n \"manifestPort\": 0,\n \"persistent\": true,\n \"portBlocked\": null,\n \"runtimePort\": 50618\n },\n \"spawnKind\": \"exe\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/fusion360\",\n \"status\": \"running\",\n \"statusVerb\": \"fusion_get_app_state\",\n \"tags\": [\n \"fusion360\",\n \"autodesk\",\n \"cad\",\n \"exports\",\n \"aps\",\n \"modeling\"\n ],\n \"timeouts\": {\n \"default\": 60,\n \"prefixes\": [\n {\n \"prefix\": \"open\",\n \"seconds\": 150\n }\n ],\n \"verbs\": {\n \"aps_open\": 150,\n \"aps_recent\": 120,\n \"aps_search\": 120,\n \"build_library_3d\": 1800,\n \"capture_library_views\": 600,\n \"close_all_documents\": 90,\n \"demo\": 300,\n \"export_3mf\": 360,\n \"export_board_image\": 90,\n \"export_bom\": 90,\n \"export_cpl\": 90,\n \"export_dwg\": 150,\n \"export_dxf\": 150,\n \"export_f3d\": 360,\n \"export_fbx\": 360,\n \"export_gerbers\": 210,\n \"export_iges\": 360,\n \"export_obj\": 360,\n \"export_sat\": 360,\n \"export_skp\": 360,\n \"export_step\": 360,\n \"export_stl\": 360,\n \"export_usdz\": 360,\n \"generate_package\": 120,\n \"get_preferences\": 25,\n \"install_fusion\": 90,\n \"make_3d_package\": 200,\n \"prewarm\": 120,\n \"run_modeling_script\": 180,\n \"search_cloud_files\": 200,\n \"set_preference\": 40,\n \"signin\": 150,\n \"start\": 90,\n \"walk_cloud_tree\": 620\n }\n },\n \"updateAvailable\": false,\n \"usageScore\": 21224.36989892864,\n \"verbCount\": 167,\n \"verbPrefixes\": [\n \"fusion_\"\n ],\n \"verbs\": [\n \"fusion_account\",\n \"fusion_action_ledger\",\n \"fusion_activate_document\",\n \"fusion_addin_status\",\n \"fusion_alerts\",\n \"fusion_apply_fusion_update\",\n \"fusion_aps_browse\",\n \"fusion_aps_create_folder\",\n \"fusion_aps_download\",\n \"fusion_aps_file_info\",\n \"fusion_aps_forget_browser\",\n \"fusion_aps_get\",\n \"fusion_aps_get_browser\",\n \"fusion_aps_open\",\n \"fusion_aps_recent\",\n \"fusion_aps_search\",\n \"fusion_aps_set_browser\",\n \"fusion_aps_set_client_id\",\n \"fusion_aps_signin\",\n \"fusion_aps_status\",\n \"fusion_aps_upload\",\n \"fusion_aps_versions\",\n \"fusion_assembly_bom\",\n \"fusion_attach_3d_package\",\n \"fusion_bind_library_3d\",\n \"fusion_board_info\",\n \"fusion_board_stackup\",\n \"fusion_build_library_3d\",\n \"fusion_cancel_run\",\n \"fusion_capture_library_views\",\n \"fusion_check_recovery\",\n \"fusion_claim_seat\",\n \"fusion_cleanup_cloud_files\",\n \"fusion_close_all_documents\",\n \"fusion_close_document\",\n \"fusion_close_window\",\n \"fusion_create_cloud_folder\",\n \"fusion_create_library\",\n \"fusion_delete_cloud_file\",\n \"fusion_demo\",\n \"fusion_demo_progress\",\n \"fusion_describe\",\n \"fusion_detect_layers\",\n \"fusion_dismiss_blocking_dialogs\",\n \"fusion_document_info\",\n \"fusion_eagle_import_status\",\n \"fusion_electron_pan\",\n \"fusion_electron_run\",\n \"fusion_electron_select\",\n \"fusion_electron_zoom\",\n \"fusion_electronics_query\",\n \"fusion_execute_text_command\",\n \"fusion_export_3mf\",\n \"fusion_export_board_image\",\n \"fusion_export_bom\",\n \"fusion_export_cloud_file\",\n \"fusion_export_cpl\",\n \"fusion_export_dwg\",\n \"fusion_export_dxf\",\n \"fusion_export_eagle_source\",\n \"fusion_export_f3d\",\n \"fusion_export_fbx\",\n \"fusion_export_gerbers\",\n \"fusion_export_iges\",\n \"fusion_export_lbr\",\n \"fusion_export_obj\",\n \"fusion_export_optimized_glb\",\n \"fusion_export_sat\",\n \"fusion_export_skp\",\n \"fusion_export_source\",\n \"fusion_export_step\",\n \"fusion_export_stl\",\n \"fusion_export_usdz\",\n \"fusion_fetch_optimized_glb\",\n \"fusion_generate_package\",\n \"fusion_get_app_state\",\n \"fusion_get_design_info\",\n \"fusion_get_parameters\",\n \"fusion_get_preferences\",\n \"fusion_import_electronics\",\n \"fusion_import_step\",\n \"fusion_inspect_bodies\",\n \"fusion_install_fusion\",\n \"fusion_install_library_bundle\",\n \"fusion_interactive\",\n \"fusion_kill\",\n \"fusion_library_manager\",\n \"fusion_library_parts\",\n \"fusion_list_cloud_files\",\n \"fusion_list_cloud_projects\",\n \"fusion_list_text_commands\",\n \"fusion_live_feed\",\n \"fusion_make_3d_package\",\n \"fusion_make_showcase_project\",\n \"fusion_make_test_project\",\n \"fusion_managed_library_create\",\n \"fusion_managed_library_link\",\n \"fusion_managed_library_pull\",\n \"fusion_managed_library_push\",\n \"fusion_managed_library_unlink\",\n \"fusion_mcp_call\",\n \"fusion_mcp_enable\",\n \"fusion_mcp_resources\",\n \"fusion_mcp_status\",\n \"fusion_mcp_tools\",\n \"fusion_mech_capabilities\",\n \"fusion_mech_operation_status\",\n \"fusion_mechanical\",\n \"fusion_narrate\",\n \"fusion_new_electronics_from_eagle\",\n \"fusion_notify_owner\",\n \"fusion_open_board\",\n \"fusion_open_by_urn\",\n \"fusion_open_by_urn_note\",\n \"fusion_open_cloud_file\",\n \"fusion_open_design\",\n \"fusion_open_electronics\",\n \"fusion_open_lbr\",\n \"fusion_open_schematic\",\n \"fusion_physical_properties\",\n \"fusion_plan_library_import\",\n \"fusion_plan_route\",\n \"fusion_preflight_lbr\",\n \"fusion_prefs_close\",\n \"fusion_prefs_open\",\n \"fusion_prewarm\",\n \"fusion_prove_result\",\n \"fusion_readiness\",\n \"fusion_release_seat\",\n \"fusion_reset_workspace\",\n \"fusion_resolve_seat_dialog\",\n \"fusion_restart_for_addin\",\n \"fusion_route_net\",\n \"fusion_routing_drc\",\n \"fusion_routing_state\",\n \"fusion_run_modeling_script\",\n \"fusion_save_to_cloud\",\n \"fusion_schematic\",\n \"fusion_screenshot_fusion\",\n \"fusion_search_cloud_files\",\n \"fusion_set_auto_update\",\n \"fusion_set_design_rules\",\n \"fusion_set_driving_badge\",\n \"fusion_set_parameter\",\n \"fusion_set_preference\",\n \"fusion_set_seat_takeover\",\n \"fusion_set_window_sizing\",\n \"fusion_show_2d_board\",\n \"fusion_show_3d_board\",\n \"fusion_show_3d_package\",\n \"fusion_show_attributes\",\n \"fusion_show_footprint\",\n \"fusion_show_library\",\n \"fusion_show_project\",\n \"fusion_show_schematic\",\n \"fusion_show_symbol\",\n \"fusion_signin\",\n \"fusion_signin_2fa\",\n \"fusion_start\",\n \"fusion_state\",\n \"fusion_stop\",\n \"fusion_take_screenshot\",\n \"fusion_uninstall\",\n \"fusion_verb_times\",\n \"fusion_view\",\n \"fusion_walk_cloud_tree\",\n \"fusion_web_signin\"\n ],\n \"version\": \"1.12.0-dev.8\",\n \"versionDrift\": true,\n \"watcher\": null\n },\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"developer-tools\",\n \"depsInstallingSec\": null,\n \"description\": \"Bridge to Hydrogen Desktop's HTTP control API. Drives setup, the Claude workspace, WSL/container lifecycle, and read-only diagnostics via direct HTTP \u2014 no shell-out, no quote mangling. HD runs as a sibling Tauri app; AD does NOT spawn it.\",\n \"devPinned\": false,\n \"displayName\": \"Hydrogen Desktop\",\n \"docs\": \"https://wiki.adom.inc/adom/adom-desktop\",\n \"entryMismatch\": false,\n \"entrypoint\": \"//?/C:/Users/drew/AppData/Local/Adom Bridge/bridges/hd/\",\n \"expectedImage\": null,\n \"heroAvailable\": false,\n \"heroDocs\": \"https://wiki.adom.inc/adom/hydrogen-desktop\",\n \"homepage\": \"https://wiki.adom.inc/adom/adom-desktop\",\n \"installPath\": \"\\\\\\\\?\\\\C:\\\\Users\\\\drew\\\\AppData\\\\Local\\\\Adom Bridge\\\\bridges\\\\hd\",\n \"installSkill\": \"adom-wiki pkg install adom/adom-desktop\",\n \"installedVersion\": \"1.0.0\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": null,\n \"languages\": [],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 0,\n \"latest\": null,\n \"livePids\": [],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/hd.log\",\n \"manifestSource\": \"updateManifestUrl\",\n \"manifestUrl\": null,\n \"manifestUrlFollowed\": null,\n \"name\": \"hd\",\n \"paused\": false,\n \"persistent\": false,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"HD currently ships Windows-only; Linux variant in scope for HD's future releases.\",\n \"supported\": false\n },\n \"macos\": {\n \"reason\": \"HD currently ships Windows-only.\",\n \"supported\": false\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": null,\n \"prewarm\": null,\n \"releasedAt\": \"2026-06-30T13:13:22Z\",\n \"runningImage\": null,\n \"runningVersion\": null,\n \"skillPkg\": \"adom/adom-desktop\",\n \"source\": \"bundled\",\n \"sourceDir\": \"//?/C:/Users/drew/AppData/Local/Adom Bridge/bridges/hd\",\n \"spawn\": {\n \"entrypoint\": \"\",\n \"instanceCount\": null,\n \"kind\": \"external-http\",\n \"manifestPort\": 47084,\n \"persistent\": false,\n \"portBlocked\": null,\n \"runtimePort\": null\n },\n \"spawnKind\": \"external-http\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/hd\",\n \"status\": \"stopped\",\n \"statusVerb\": null,\n \"tags\": [\n \"hydrogen-desktop\",\n \"control-api\",\n \"http\",\n \"lifecycle\"\n ],\n \"timeouts\": null,\n \"updateAvailable\": false,\n \"usageScore\": 0.0,\n \"verbCount\": 11,\n \"verbPrefixes\": [],\n \"verbs\": [\n \"hd_api\",\n \"hd_status\",\n \"hd_launch\",\n \"hd_stop\",\n \"hd_restart\",\n \"hd_log\",\n \"hd_screenshot\",\n \"hd_shot\",\n \"hd_open_url\",\n \"hd_browser_profiles\",\n \"hd_ship\"\n ],\n \"version\": \"1.0.0\",\n \"versionDrift\": false,\n \"watcher\": null\n },\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"EDA\",\n \"depsInstallingSec\": null,\n \"description\": \"Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.\",\n \"devPinned\": false,\n \"displayName\": \"KiCad EDA\",\n \"docs\": \"https://wiki.adom.inc/adom/kicad-bridge\",\n \"entryMismatch\": false,\n \"entrypoint\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/kicad/kicad-bridge.exe\",\n \"expectedImage\": \"kicad-bridge.exe\",\n \"heroAvailable\": false,\n \"heroDocs\": null,\n \"homepage\": \"https://wiki.adom.inc/adom/adom-bridge\",\n \"installPath\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/kicad\",\n \"installSkill\": \"adom-wiki pkg install adom/kicad-bridge\",\n \"installedVersion\": \"1.0.55\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": 1,\n \"languages\": [\n \"Python\",\n \"PowerShell\"\n ],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 1790956354547,\n \"latest\": \"1.0.55\",\n \"livePids\": [\n 36872\n ],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/kicad.log\",\n \"manifestSource\": \"insidersManifestUrl\",\n \"manifestUrl\": \"https://wiki.adom.inc/api/v1/pages/kicad-bridge/files/adom-bridge-kicad-manifest.json\",\n \"manifestUrlFollowed\": null,\n \"name\": \"kicad\",\n \"paused\": false,\n \"persistent\": false,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"\",\n \"supported\": false\n },\n \"macos\": {\n \"reason\": \"\",\n \"supported\": false\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": 60896,\n \"prewarm\": {\n \"assets\": [\n \"kicad-cli warm (detection)\",\n \"KiCad first-run config + setup wizard marked done\",\n \"KiCad's own taskbar shortcut identity\"\n ],\n \"lastRun\": {\n \"at\": \"2026-10-05T15:41:58Z\",\n \"elapsedMs\": 273,\n \"error\": null,\n \"ok\": false,\n \"verb\": \"kicad_prewarm\",\n \"version\": \"1.0.55\"\n },\n \"requiresHostApp\": true,\n \"timeoutSeconds\": null,\n \"verb\": \"kicad_prewarm\"\n },\n \"releasedAt\": \"2026-06-26T16:00:00Z\",\n \"runningImage\": \"kicad-bridge.exe\",\n \"runningVersion\": \"1.0.55\",\n \"skillPkg\": \"adom/kicad-bridge\",\n \"source\": \"cache\",\n \"sourceDir\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/kicad\",\n \"spawn\": {\n \"entrypoint\": \"kicad-bridge.exe\",\n \"instanceCount\": 1,\n \"kind\": \"exe\",\n \"manifestPort\": 0,\n \"persistent\": false,\n \"portBlocked\": null,\n \"runtimePort\": 60896\n },\n \"spawnKind\": \"exe\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/kicad\",\n \"status\": \"running\",\n \"statusVerb\": \"kicad_status\",\n \"tags\": [\n \"pcb\",\n \"schematic\",\n \"open-source\",\n \"kicad-10\",\n \"kicad-11-ready\"\n ],\n \"timeouts\": {\n \"default\": 130,\n \"prefixes\": [],\n \"verbs\": {\n \"autoroute\": 900,\n \"demo\": 600,\n \"enable_software_opengl\": 240,\n \"export_gerber\": 210,\n \"export_molecule\": 420,\n \"export_pdf\": 180,\n \"export_step\": 300,\n \"export_svg\": 180,\n \"format_upgrade\": 180,\n \"freerouting\": 900,\n \"open_3d_viewer\": 300,\n \"prewarm\": 120,\n \"run_drc\": 300,\n \"run_erc\": 300,\n \"upgrade\": 900\n }\n },\n \"updateAvailable\": false,\n \"usageScore\": 1037.9305550728664,\n \"verbCount\": 119,\n \"verbPrefixes\": [\n \"kicad_\"\n ],\n \"verbs\": [\n \"kicad_describe\",\n \"kicad_status\",\n \"kicad_readiness\",\n \"kicad_list_versions\",\n \"kicad_diagnostics\",\n \"kicad_run_drc\",\n \"kicad_run_erc\",\n \"kicad_export_gerber\",\n \"kicad_export_pdf\",\n \"kicad_export_svg\",\n \"kicad_export_step\",\n \"kicad_export_bom_csv\",\n \"kicad_uninstall\",\n \"kicad_extract_netlist\",\n \"kicad_trace_net\",\n \"kicad_find_connections\",\n \"kicad_analyze_connections\",\n \"kicad_lint_board\",\n \"kicad_lint_schematic\",\n \"kicad_lint_library\",\n \"kicad_format_upgrade\",\n \"kicad_list_symbols\",\n \"kicad_list_footprints\",\n \"kicad_adom_library_status\",\n \"kicad_get_settings\",\n \"kicad_set_settings\",\n \"kicad_list_design_rules\",\n \"kicad_set_design_rules\",\n \"kicad_3d_viewer_settings\",\n \"kicad_taskbar_repair\",\n \"kicad_prewarm\",\n \"kicad_model_check\",\n \"kicad_pcm_list\",\n \"kicad_pcm_install\",\n \"kicad_pcm_uninstall\",\n \"kicad_pcm_add_repository\",\n \"kicad_pcm_remove_repository\",\n \"kicad_check_for_updates\",\n \"kicad_upgrade\",\n \"kicad_sch_place_symbol\",\n \"kicad_sch_wire\",\n \"kicad_sch_label\",\n \"kicad_sch_move\",\n \"kicad_sch_delete\",\n \"kicad_sch_set_property\",\n \"kicad_ipc_api\",\n \"kicad_export_molecule\",\n \"kicad_errors\",\n \"kicad_log_tail\",\n \"kicad_bridge_status\",\n \"kicad_bridge_call\",\n \"kicad_install_plugin\",\n \"kicad_plugin_diagnose\",\n \"kicad_install_library\",\n \"kicad_uninstall_library\",\n \"kicad_install_symbol\",\n \"kicad_install_footprint\",\n \"kicad_install_library_bundle\",\n \"kicad_adom_library_heal\",\n \"kicad_make_part_project\",\n \"kicad_export_part\",\n \"kicad_silk_fields_batch\",\n \"kicad_silk_graphics_batch\",\n \"kicad_text_bounds\",\n \"kicad_silk_text_batch\",\n \"kicad_routing_state\",\n \"kicad_route_net\",\n \"kicad_remove_route\",\n \"kicad_add_zone\",\n \"kicad_apply_zone_plan\",\n \"kicad_zone_state\",\n \"kicad_select_net\",\n \"kicad_select_clear\",\n \"kicad_remove_zone\",\n \"kicad_routing_validate\",\n \"kicad_board_pads\",\n \"kicad_add_track\",\n \"kicad_add_via\",\n \"kicad_route\",\n \"kicad_autoroute\",\n \"kicad_freerouting\",\n \"kicad_placement_state\",\n \"kicad_move_footprint\",\n \"kicad_placement_validate\",\n \"kicad_refresh_board_views\",\n \"kicad_launch\",\n \"kicad_open_board\",\n \"kicad_open_schematic\",\n \"kicad_open_symbol_editor\",\n \"kicad_open_footprint_editor\",\n \"kicad_menu_items\",\n \"kicad_3d_view\",\n \"kicad_open_3d_viewer\",\n \"kicad_open_editors\",\n \"kicad_close\",\n \"kicad_close_symbol_editor\",\n \"kicad_close_footprint_editor\",\n \"kicad_close_3d_viewer\",\n \"kicad_dismiss_dialogs\",\n \"kicad_window_info\",\n \"kicad_state\",\n \"kicad_screenshot_all\",\n \"kicad_send_key\",\n \"kicad_click\",\n \"kicad_fix_keyboard\",\n \"kicad_place_footprint\",\n \"kicad_enable_software_opengl\",\n \"kicad_rescan_libraries\",\n \"kicad_show_project\",\n \"kicad_show_symbol\",\n \"kicad_show_footprint\",\n \"kicad_show_3d_chip\",\n \"kicad_show_schematic\",\n \"kicad_show_2d_board\",\n \"kicad_show_3d_board\",\n \"kicad_show_library\",\n \"kicad_progress\",\n \"kicad_verb_times\",\n \"kicad_demo\"\n ],\n \"version\": \"1.0.55\",\n \"versionDrift\": false,\n \"watcher\": {\n \"configKey\": \"project_watch\",\n \"defaultGlobs\": [\n \"*.kicad_pcb\",\n \"*.kicad_sch\",\n \"*.kicad_pro\"\n ],\n \"description\": \"Monitors a KiCad project folder for file changes and syncs them to Docker. Configured via the set_project_watch / trigger_project_watch verbs (AD core owns the watcher; the KiCad section of AD's Settings panel was removed in AD 1.9.117 \u2014 the config keys are still honored).\",\n \"displayName\": \"Project Watch\",\n \"supports\": true\n }\n },\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"Browser Automation\",\n \"depsInstallingSec\": null,\n \"description\": \"Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.\",\n \"devPinned\": false,\n \"displayName\": \"Adom Native Browser\",\n \"docs\": \"https://wiki.adom.inc/adom/nb-bridge\",\n \"entryMismatch\": false,\n \"entrypoint\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/native-browser/server.js\",\n \"expectedImage\": \"node.exe\",\n \"heroAvailable\": false,\n \"heroDocs\": null,\n \"homepage\": \"https://wiki.adom.inc/adom/nb-bridge\",\n \"installPath\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/native-browser\",\n \"installSkill\": \"adom-wiki pkg install adom/nb-bridge\",\n \"installedVersion\": \"0.16.33\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": 1,\n \"languages\": [\n \"Node/JS\"\n ],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 1791220671752,\n \"latest\": \"0.16.33\",\n \"livePids\": [\n 32228\n ],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/native-browser.log\",\n \"manifestSource\": \"insidersManifestUrl\",\n \"manifestUrl\": \"https://wiki.adom.inc/api/pages/adom/nb-bridge/files/native-browser-bridge-manifest.json\",\n \"manifestUrlFollowed\": null,\n \"name\": \"native-browser\",\n \"paused\": false,\n \"persistent\": true,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"Pending; bridge + host are OS-agnostic Node, only the install/registration layer is Windows-specific.\",\n \"supported\": false\n },\n \"macos\": {\n \"reason\": \"Windows-first; Kyle ports to macOS as a fast-follow (NativeMessagingHosts live under ~/Library/Application Support//NativeMessagingHosts; bridge/host are OS-agnostic Node).\",\n \"supported\": false\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": 51703,\n \"prewarm\": {\n \"assets\": [],\n \"lastRun\": {\n \"at\": \"2026-09-22T19:44:55Z\",\n \"elapsedMs\": 41,\n \"error\": null,\n \"ok\": true,\n \"verb\": \"nbrowser_prewarm\",\n \"version\": \"0.16.33\"\n },\n \"requiresHostApp\": false,\n \"timeoutSeconds\": null,\n \"verb\": \"nbrowser_prewarm\"\n },\n \"releasedAt\": \"2026-09-22T15:18:44Z\",\n \"runningImage\": \"node.exe\",\n \"runningVersion\": \"0.16.33\",\n \"skillPkg\": \"adom/nb-bridge\",\n \"source\": \"cache\",\n \"sourceDir\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/native-browser\",\n \"spawn\": {\n \"entrypoint\": \"server.js\",\n \"instanceCount\": 1,\n \"kind\": \"node\",\n \"manifestPort\": 0,\n \"persistent\": true,\n \"portBlocked\": null,\n \"runtimePort\": 51703\n },\n \"spawnKind\": \"node\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/native-browser\",\n \"status\": \"running\",\n \"statusVerb\": \"nbrowser_status\",\n \"tags\": [\n \"nb\",\n \"nbe\",\n \"native-browser\",\n \"chrome\",\n \"edge\",\n \"nbrowser\",\n \"pup-parity\"\n ],\n \"timeouts\": {\n \"default\": 60,\n \"prefixes\": [\n {\n \"prefix\": \"screenshot\",\n \"seconds\": 120\n },\n {\n \"prefix\": \"record\",\n \"seconds\": 120\n }\n ],\n \"verbs\": {\n \"fetch_url\": 120,\n \"input_dispatch\": 120,\n \"select\": 30,\n \"wait\": 125\n }\n },\n \"updateAvailable\": false,\n \"usageScore\": 917.1856854627648,\n \"verbCount\": 88,\n \"verbPrefixes\": [\n \"nbrowser_\"\n ],\n \"verbs\": [\n \"nbrowser_status\",\n \"nbrowser_readiness\",\n \"nbrowser_install_chrome\",\n \"nbrowser_prewarm\",\n \"nbrowser_chrome_status\",\n \"nbrowser_ping\",\n \"nbrowser_progress\",\n \"nbrowser_activity\",\n \"nbrowser_open_window\",\n \"nbrowser_close_window\",\n \"nbrowser_owned\",\n \"nbrowser_cleanup\",\n \"nbrowser_force_close\",\n \"nbrowser_switch_window\",\n \"nbrowser_list_windows\",\n \"nbrowser_window_state\",\n \"nbrowser_fullscreen\",\n \"nbrowser_login_state\",\n \"nbrowser_credentials\",\n \"nbrowser_profiles\",\n \"nbrowser_use_profile\",\n \"nbrowser_pin_session\",\n \"nbrowser_open_os_window\",\n \"nbrowser_os_windows\",\n \"nbrowser_wake_profile\",\n \"nbrowser_resolve_profiles\",\n \"nbrowser_whoami\",\n \"nbrowser_profile_block\",\n \"nbrowser_profile_unblock\",\n \"nbrowser_rescan\",\n \"nbrowser_resync_sessions\",\n \"nbrowser_open_tab\",\n \"nbrowser_close_tab\",\n \"nbrowser_switch_tab\",\n \"nbrowser_list_tabs\",\n \"nbrowser_navigate\",\n \"nbrowser_reload\",\n \"nbrowser_back\",\n \"nbrowser_forward\",\n \"nbrowser_wait\",\n \"nbrowser_wait_for\",\n \"nbrowser_keepalive\",\n \"nbrowser_screenshot\",\n \"nbrowser_screenshot_full_res\",\n \"nbrowser_eval\",\n \"nbrowser_evaluate\",\n \"nbrowser_input_dispatch\",\n \"nbrowser_scroll\",\n \"nbrowser_click\",\n \"nbrowser_select\",\n \"nbrowser_set_file_input\",\n \"nbrowser_set_date\",\n \"nbrowser_totp\",\n \"nbrowser_register_totp\",\n \"nbrowser_add_extension_origin\",\n \"nbrowser_type\",\n \"nbrowser_press_key\",\n \"nbrowser_fetch_url\",\n \"nbrowser_download_wait\",\n \"nbrowser_errors\",\n \"nbrowser_close\",\n \"nbrowser_record_start\",\n \"nbrowser_record_stop\",\n \"nbrowser_record_status\",\n \"nbrowser_record_list\",\n \"nbrowser_flash\",\n \"nbrowser_flash_stop\",\n \"nbrowser_apply_settings\",\n \"nbrowser_settings\",\n \"nbrowser_flash_mode\",\n \"nbrowser_taskbar\",\n \"nbrowser_describe\",\n \"nbrowser_handle_dialog\",\n \"nbrowser_state\",\n \"nbrowser_demo\",\n \"nbrowser_uninstall\",\n \"nbrowser_dev_reload\",\n \"nbrowser_clear_cookies\",\n \"nbrowser_caption\",\n \"nbrowser_focus\",\n \"nbrowser_hover\",\n \"nbrowser_double_click\",\n \"nbrowser_right_click\",\n \"nbrowser_login_form\",\n \"nbrowser_autologin\",\n \"nbrowser_cursor\",\n \"nbrowser_events\",\n \"nbrowser_download\"\n ],\n \"version\": \"0.16.33\",\n \"versionDrift\": false,\n \"watcher\": null\n },\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"Manufacturing\",\n \"depsInstallingSec\": null,\n \"description\": \"The Adom Desktop bridge for the Essemtec Puma / ePlace. Reads + dry-run: machine state, recipe/package/component/feeder reads, the full feeder fleet from Supply/Feeder/*.feeder.xml, and dry-run of any WriteOp (preflight + physics-bounds + unified diff). F4 adds puma_write \u2014 GATED production writes of FileWrite.* library objects to real XML files under --data-folder (atomic write + XML-verify + audit; approver enforced; refused when the machine is Productive), plus puma_revert \u2014 canonical revert of a prior write by correlationId (CAS-pinned to the recorded result hash, its own audit entry, same write gate). Off by default: requires --enable-writes. eData stays vendor-read-only. 0.2.5: puma_feeders binds each loaded lane to its component/part number (carrierType, componentName, partValue, packageName, componentBarcode) via the Supply/Carrier -> Component library chain (#639). 0.2.6: the write audit records ab's X-Adom-Verified-Origin (relay | local | bridge, plus the X-Adom-Caller-Delegate chain for a bridge callback; a missing header records as local-unattributed) beside the approver \u2014 attribution, never a gate. New puma_write_status (declared as statusVerb): what happened to a write by correlationId, so a caller whose transport timed out polls instead of declaring the write unknown. 0.2.7: the host is a GUI-subsystem exe (no console window of its own) and its git subprocess runs with CreateNoWindow, so the bridge can never leave a conhost window on the desktop across ab respawns or reinstalls.\",\n \"devPinned\": false,\n \"displayName\": \"Puma Bridge (Essemtec ePlace)\",\n \"docs\": \"https://wiki.adom.inc/adom/puma-bridge\",\n \"entryMismatch\": false,\n \"entrypoint\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puma-bridge/puma-bridge-host.exe\",\n \"expectedImage\": \"puma-bridge-host.exe\",\n \"heroAvailable\": false,\n \"heroDocs\": null,\n \"homepage\": \"https://wiki.adom.inc/adom/puma-bridge\",\n \"installPath\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puma-bridge\",\n \"installSkill\": \"adom-wiki pkg install adom/puma-bridge\",\n \"installedVersion\": \"0.2.7\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": 1,\n \"languages\": [\n \"Binary\"\n ],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 1790623763822,\n \"latest\": \"0.2.6\",\n \"livePids\": [\n 32008\n ],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/puma-bridge.log\",\n \"manifestSource\": \"updateManifestUrl\",\n \"manifestUrl\": \"https://wiki.adom.inc/api/v1/pages/adom/puma-bridge/files/adom-bridge-puma-manifest.json\",\n \"manifestUrlFollowed\": null,\n \"name\": \"puma-bridge\",\n \"paused\": false,\n \"persistent\": true,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"\",\n \"supported\": true\n },\n \"macos\": {\n \"reason\": \"\",\n \"supported\": false\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": 64764,\n \"prewarm\": null,\n \"releasedAt\": \"2026-09-23T15:54:11Z\",\n \"runningImage\": \"puma-bridge-host.exe\",\n \"runningVersion\": \"0.2.7\",\n \"skillPkg\": \"adom/puma-bridge\",\n \"source\": \"cache\",\n \"sourceDir\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puma-bridge\",\n \"spawn\": {\n \"entrypoint\": \"puma-bridge-host.exe\",\n \"instanceCount\": 1,\n \"kind\": \"exe\",\n \"manifestPort\": 0,\n \"persistent\": true,\n \"portBlocked\": null,\n \"runtimePort\": 64764\n },\n \"spawnKind\": \"exe\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/puma-bridge\",\n \"status\": \"running\",\n \"statusVerb\": \"puma_write_status\",\n \"tags\": [\n \"smt\",\n \"essemtec\",\n \"puma\",\n \"eplace\",\n \"pick-and-place\",\n \"manufacturing\",\n \"factoryos\"\n ],\n \"timeouts\": null,\n \"updateAvailable\": false,\n \"usageScore\": 2.691439652959016,\n \"verbCount\": 9,\n \"verbPrefixes\": [\n \"puma_\"\n ],\n \"verbs\": [\n \"puma_ping\",\n \"puma_state\",\n \"puma_get\",\n \"puma_feeders\",\n \"puma_dryrun\",\n \"puma_write\",\n \"puma_revert\",\n \"puma_write_status\",\n \"puma_describe\"\n ],\n \"version\": \"0.2.7\",\n \"versionDrift\": false,\n \"watcher\": null\n },\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"Browser Automation\",\n \"depsInstallingSec\": null,\n \"description\": \"Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.\",\n \"devPinned\": false,\n \"displayName\": \"Puppeteer (Chrome for Testing + Native)\",\n \"docs\": \"https://wiki.adom.inc/adom/pup-bridge\",\n \"entryMismatch\": false,\n \"entrypoint\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puppeteer/adom-pup-bridge.exe\",\n \"expectedImage\": \"adom-pup-bridge.exe\",\n \"heroAvailable\": false,\n \"heroDocs\": null,\n \"homepage\": \"https://wiki.adom.inc/adom/adom-bridge\",\n \"installPath\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puppeteer\",\n \"installSkill\": \"adom-wiki pkg install adom/pup-bridge\",\n \"installedVersion\": \"2.0.697\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": 1,\n \"languages\": [\n \"Binary\"\n ],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 1791212202067,\n \"latest\": \"2.0.697\",\n \"livePids\": [\n 68812\n ],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/puppeteer.log\",\n \"manifestSource\": \"insidersManifestUrl\",\n \"manifestUrl\": \"https://wiki.adom.inc/api/v1/pages/pup-bridge/files/adom-bridge-puppeteer-manifest.json\",\n \"manifestUrlFollowed\": null,\n \"name\": \"puppeteer\",\n \"paused\": false,\n \"persistent\": true,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"not yet\",\n \"supported\": false\n },\n \"macos\": {\n \"reason\": \"The Rust exe is Windows-only for now; host-mac is stubbed for the Mac port. macOS runs the Node build (public tier).\",\n \"supported\": false\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": 57134,\n \"prewarm\": {\n \"assets\": [\n \"chrome-for-testing\"\n ],\n \"lastRun\": {\n \"at\": \"2026-10-05T14:55:58Z\",\n \"elapsedMs\": 1409,\n \"error\": null,\n \"ok\": true,\n \"verb\": \"pup_prewarm\",\n \"version\": \"2.0.697\"\n },\n \"requiresHostApp\": false,\n \"timeoutSeconds\": 900,\n \"verb\": \"pup_prewarm\"\n },\n \"releasedAt\": \"2026-10-05T14:55:48Z\",\n \"runningImage\": \"adom-pup-bridge.exe\",\n \"runningVersion\": \"2.0.697\",\n \"skillPkg\": \"adom/pup-bridge\",\n \"source\": \"cache\",\n \"sourceDir\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puppeteer\",\n \"spawn\": {\n \"entrypoint\": \"adom-pup-bridge.exe\",\n \"instanceCount\": 1,\n \"kind\": \"exe\",\n \"manifestPort\": 8851,\n \"persistent\": true,\n \"portBlocked\": null,\n \"runtimePort\": 57134\n },\n \"spawnKind\": \"exe\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/puppeteer\",\n \"status\": \"running\",\n \"statusVerb\": \"pup_readiness\",\n \"tags\": [\n \"puppeteer\",\n \"chrome\",\n \"headful\",\n \"recording\"\n ],\n \"timeouts\": {\n \"default\": 60,\n \"prefixes\": [\n {\n \"prefix\": \"record\",\n \"seconds\": 90\n }\n ],\n \"verbs\": {\n \"credential_reveal\": 150,\n \"fill_card\": 150,\n \"open_window\": 150,\n \"prewarm\": 240,\n \"use\": 240\n }\n },\n \"updateAvailable\": false,\n \"usageScore\": 580.1076328182536,\n \"verbCount\": 97,\n \"verbPrefixes\": [\n \"pup_\",\n \"browser_\"\n ],\n \"verbs\": [\n \"pup_describe\",\n \"pup_readiness\",\n \"pup_prewarm\",\n \"pup_status\",\n \"pup_list_windows\",\n \"pup_my_windows\",\n \"pup_list_sessions\",\n \"pup_open_window\",\n \"pup_navigate\",\n \"pup_eval\",\n \"pup_evaluate\",\n \"pup_screenshot\",\n \"pup_close_window\",\n \"pup_close\",\n \"pup_close_all_windows\",\n \"pup_window_info\",\n \"pup_open_tab\",\n \"pup_list_tabs\",\n \"pup_switch_tab\",\n \"pup_close_tab\",\n \"pup_switch_window\",\n \"pup_click\",\n \"pup_select\",\n \"pup_hover\",\n \"pup_type\",\n \"pup_press_key\",\n \"pup_scroll\",\n \"pup_fill\",\n \"pup_wait\",\n \"pup_reload\",\n \"pup_back\",\n \"pup_forward\",\n \"pup_set_viewport\",\n \"pup_errors\",\n \"pup_dialog\",\n \"pup_input_dispatch\",\n \"pup_render_html\",\n \"pup_fetch_url\",\n \"pup_maximize\",\n \"pup_raise_os_window\",\n \"pup_focus_window\",\n \"pup_lower_os_window\",\n \"pup_alert_window\",\n \"pup_progress\",\n \"pup_configure\",\n \"pup_use\",\n \"pup_clear_cookies\",\n \"pup_clear_site_data\",\n \"pup_quit_idle_browsers\",\n \"pup_rescan\",\n \"pup_restart_browser\",\n \"pup_set_identity\",\n \"pup_audit_icons\",\n \"pup_dashboard\",\n \"pup_credentials\",\n \"credential_list\",\n \"credential_set\",\n \"credential_delete\",\n \"pup_login\",\n \"pup_google_accounts\",\n \"pup_google_signin\",\n \"pup_credential_reveal\",\n \"pup_card_set\",\n \"pup_cards\",\n \"pup_card_delete\",\n \"pup_fill_card\",\n \"pup_wiki_set_view\",\n \"pup_annotate\",\n \"pup_floaty_click\",\n \"pup_record_start\",\n \"pup_record_stop\",\n \"pup_record_status\",\n \"pup_record_list\",\n \"pup_recorder_open\",\n \"pup_recorder_close\",\n \"pup_screen_record_start\",\n \"pup_screen_record_stop\",\n \"pup_screen_record_status\",\n \"pup_screen_record_list\",\n \"pup_highfps_extend\",\n \"pup_screenshot_element\",\n \"pup_screenshot_full_res\",\n \"pup_download\",\n \"pup_launch\",\n \"pup_deps\",\n \"pup_install_toolbar\",\n \"pup_reap_profiles\",\n \"pup_cache_prune\",\n \"pup_sweep_identities\",\n \"pup_uninstall\",\n \"pup_move_to_workspace\",\n \"pup_assisted_click\",\n \"pup_import_scan\",\n \"pup_import_browser_logins\",\n \"pup_vault_sync\",\n \"pup_vault_export\",\n \"pup_vault_import\"\n ],\n \"version\": \"2.0.697\",\n \"versionDrift\": false,\n \"watcher\": null\n },\n {\n \"author\": \"Adom Inc.\",\n \"category\": \"MCAD\",\n \"depsInstallingSec\": null,\n \"description\": \"Bridge for SOLIDWORKS desktop CAD \u2014 create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.\",\n \"devPinned\": false,\n \"displayName\": \"SOLIDWORKS\",\n \"docs\": \"https://wiki.adom.inc/adom/solidworks-bridge\",\n \"entryMismatch\": false,\n \"entrypoint\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/solidworks/swx-bridge.exe\",\n \"expectedImage\": \"swx-bridge.exe\",\n \"heroAvailable\": false,\n \"heroDocs\": null,\n \"homepage\": \"https://wiki.adom.inc/adom/adom-bridge\",\n \"installPath\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/solidworks\",\n \"installSkill\": \"adom-wiki pkg install adom/solidworks-bridge\",\n \"installedVersion\": \"0.9.21\",\n \"installedVersionSource\": \"cache\",\n \"instanceCount\": 0,\n \"languages\": [\n \"Binary\"\n ],\n \"lastSpawnError\": null,\n \"lastUsedMs\": 1791224978746,\n \"latest\": \"0.9.19\",\n \"livePids\": [],\n \"logPath\": \"C:/Users/drew/.adom/bridge-logs/solidworks.log\",\n \"manifestSource\": \"updateManifestUrl\",\n \"manifestUrl\": \"https://wiki.adom.inc/api/v1/pages/solidworks-bridge/files/adom-bridge-solidworks-manifest.json\",\n \"manifestUrlFollowed\": null,\n \"name\": \"solidworks\",\n \"paused\": false,\n \"persistent\": false,\n \"platforms\": {\n \"linux\": {\n \"reason\": \"SOLIDWORKS desktop is Windows-only; the bridge drives its COM API.\",\n \"supported\": false\n },\n \"macos\": {\n \"reason\": \"SOLIDWORKS desktop is Windows-only; the bridge drives its COM API.\",\n \"supported\": false\n },\n \"windows\": {\n \"reason\": \"\",\n \"supported\": true\n }\n },\n \"port\": 49861,\n \"prewarm\": null,\n \"releasedAt\": \"2026-10-05T18:49:46Z\",\n \"runningImage\": null,\n \"runningVersion\": \"0.9.19\",\n \"skillPkg\": \"adom/solidworks-bridge\",\n \"source\": \"cache\",\n \"sourceDir\": \"C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/solidworks\",\n \"spawn\": {\n \"entrypoint\": \"swx-bridge.exe\",\n \"instanceCount\": 0,\n \"kind\": \"exe\",\n \"manifestPort\": 0,\n \"persistent\": false,\n \"portBlocked\": null,\n \"runtimePort\": 49861\n },\n \"spawnKind\": \"exe\",\n \"stableUrl\": \"http://127.0.0.1:24680/open/solidworks\",\n \"status\": \"running\",\n \"statusVerb\": \"swx_status\",\n \"tags\": [\n \"solidworks\",\n \"mcad\",\n \"cad\",\n \"bom\",\n \"step\",\n \"mass-properties\",\n \"modeling\",\n \"fusion-parity\"\n ],\n \"timeouts\": {\n \"default\": 120,\n \"prefixes\": [],\n \"verbs\": {\n \"action_ledger\": 20,\n \"activate_document\": 180,\n \"bom\": 300,\n \"check_recovery\": 180,\n \"close_all_documents\": 240,\n \"close_all_safely\": 600,\n \"close_doc\": 180,\n \"close_import_temps\": 600,\n \"close_window\": 120,\n \"cloud_status\": 90,\n \"design_info\": 180,\n \"dismiss_blocking_dialogs\": 180,\n \"document_info\": 120,\n \"export\": 300,\n \"export_3mf\": 300,\n \"export_dwg\": 300,\n \"export_dxf\": 300,\n \"export_glb\": 300,\n \"export_iges\": 300,\n \"export_parasolid\": 300,\n \"export_pdf\": 300,\n \"export_sat\": 300,\n \"export_step\": 300,\n \"export_stl\": 300,\n \"find_files\": 180,\n \"get_app_state\": 90,\n \"get_parameters\": 240,\n \"get_preferences\": 90,\n \"import\": 420,\n \"inspect_bodies\": 180,\n \"interference\": 600,\n \"kill\": 240,\n \"new_document\": 180,\n \"open\": 300,\n \"pack_and_go\": 600,\n \"rebuild_check\": 300,\n \"render_image\": 600,\n \"reset_workspace\": 600,\n \"run_script\": 420,\n \"save_as\": 420,\n \"set_driving_badge\": 120,\n \"set_parameter\": 420,\n \"set_preference\": 90,\n \"show_assembly\": 600,\n \"show_drawing\": 600,\n \"show_part\": 600,\n \"shutdown\": 900,\n \"sign_in\": 240,\n \"start\": 300,\n \"state\": 180,\n \"stop\": 420,\n \"verb_times\": 20,\n \"window_info\": 120\n }\n },\n \"updateAvailable\": false,\n \"usageScore\": 561.3708117917515,\n \"verbCount\": 69,\n \"verbPrefixes\": [\n \"swx_\"\n ],\n \"verbs\": [\n \"swx_action_ledger\",\n \"swx_activate_document\",\n \"swx_add_component\",\n \"swx_bom\",\n \"swx_check_recovery\",\n \"swx_close_all_documents\",\n \"swx_close_all_safely\",\n \"swx_close_doc\",\n \"swx_close_import_temps\",\n \"swx_close_window\",\n \"swx_cloud_status\",\n \"swx_com_eval\",\n \"swx_describe\",\n \"swx_design_info\",\n \"swx_dismiss_blocking_dialogs\",\n \"swx_docs\",\n \"swx_document_info\",\n \"swx_export\",\n \"swx_export_3mf\",\n \"swx_export_dwg\",\n \"swx_export_dxf\",\n \"swx_export_glb\",\n \"swx_export_iges\",\n \"swx_export_parasolid\",\n \"swx_export_pdf\",\n \"swx_export_sat\",\n \"swx_export_step\",\n \"swx_export_stl\",\n \"swx_find_files\",\n \"swx_get_app_state\",\n \"swx_get_parameters\",\n \"swx_get_preferences\",\n \"swx_import\",\n \"swx_inspect_bodies\",\n \"swx_interference\",\n \"swx_kill\",\n \"swx_mass_properties\",\n \"swx_new_document\",\n \"swx_open\",\n \"swx_pack_and_go\",\n \"swx_progress\",\n \"swx_props_get\",\n \"swx_props_set\",\n \"swx_readiness\",\n \"swx_rebuild_check\",\n \"swx_recover_list\",\n \"swx_render_image\",\n \"swx_reset_workspace\",\n \"swx_run_macro\",\n \"swx_run_script\",\n \"swx_save\",\n \"swx_save_as\",\n \"swx_screenshot\",\n \"swx_set_driving_badge\",\n \"swx_set_parameter\",\n \"swx_set_preference\",\n \"swx_show_assembly\",\n \"swx_show_drawing\",\n \"swx_show_part\",\n \"swx_shutdown\",\n \"swx_sign_in\",\n \"swx_start\",\n \"swx_state\",\n \"swx_status\",\n \"swx_stop\",\n \"swx_uninstall\",\n \"swx_user_presence\",\n \"swx_verb_times\",\n \"swx_window_info\"\n ],\n \"version\": \"0.9.21\",\n \"versionDrift\": true,\n \"watcher\": null\n }\n ],\n \"current_os\": \"windows\",\n \"durationMs\": 23,\n \"error\": \"\",\n \"invalidCount\": 0,\n \"ok\": true,\n \"output\": \"\",\n \"schemaVersion\": 1,\n \"staleCheckedAt\": 1791215191,\n \"status\": \"ok\",\n \"success\": true,\n \"summary\": {\n \"bundled\": 1,\n \"cache\": 6,\n \"invalid\": 0,\n \"paused\": 0,\n \"third_party\": 0,\n \"total\": 7\n },\n \"timeoutSeconds\": 10\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235896.9252222, + "raw": { + "_hint": "Per-bridge runtime info. `source` tells you where the bridge code lives: bundled (installer ships it) / cache (wiki-streamed, writable) / third_party (installed via bridge_install). Use bridge_install to add a new bridge from a wiki manifest URL, bridge_pause/resume to control lifecycle without uninstalling, bridge_uninstall to remove. v1.8.0+.", + "_skillsHint": "For each bridge here with skillPkg != null, the container should run its `installSkill` to get that bridge's SKILL.md into ~/.claude/skills/ (then `adom-wiki pkg update` keeps them current). skillPkg:null bridges ship their skill in the adom-bridge core pkg (or have none).", + "bridges": [ + { + "author": "Adom Inc.", + "category": "CAD", + "depsInstallingSec": null, + "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", + "devPinned": true, + "displayName": "Autodesk Fusion 360", + "docs": "https://wiki.adom.inc/adom/fusion-bridge", + "entryMismatch": false, + "entrypoint": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/fusion360/adom-fusion-bridge.exe", + "expectedImage": "adom-fusion-bridge.exe", + "heroAvailable": false, + "heroDocs": null, + "homepage": "https://wiki.adom.inc/adom/fusion-bridge", + "installPath": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/fusion360", + "installSkill": "adom-wiki pkg install adom/fusion-bridge", + "installedVersion": "1.12.0-dev.8", + "installedVersionSource": "cache", + "instanceCount": 1, + "languages": [ + "Python" + ], + "lastSpawnError": null, + "lastUsedMs": 1791235896837, + "latest": null, + "livePids": [ + 42780 + ], + "logPath": "C:/Users/drew/.adom/bridge-logs/fusion360.log", + "manifestSource": null, + "manifestUrl": "https://wiki.adom.inc/api/v1/pages/fusion-bridge/files/adom-bridge-fusion-manifest.json", + "manifestUrlFollowed": null, + "name": "fusion360", + "paused": false, + "persistent": true, + "platforms": { + "linux": { + "reason": "Autodesk doesn't ship Fusion 360 for Linux, so there's no host app for the add-in to attach to.", + "supported": false + }, + "macos": { + "reason": "Fusion 360 runs on macOS; bridge + add-in install paths handled by fusion_detect.py.", + "supported": true + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": 50618, + "prewarm": { + "assets": [ + "AdomBridge add-in sync", + "PCB design-rules catalog cache" + ], + "lastRun": { + "at": "2026-10-05T15:41:57Z", + "elapsedMs": 2344, + "error": null, + "ok": true, + "verb": "fusion_prewarm", + "version": "1.12.0-dev.5" + }, + "requiresHostApp": true, + "timeoutSeconds": null, + "verb": "fusion_prewarm" + }, + "releasedAt": "2026-06-26T06:00:00Z", + "runningImage": "adom-fusion-bridge.exe", + "runningVersion": "1.12.0-dev.5", + "skillPkg": "adom/fusion-bridge", + "source": "cache", + "sourceDir": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/fusion360", + "spawn": { + "entrypoint": "adom-fusion-bridge.exe", + "instanceCount": 1, + "kind": "exe", + "manifestPort": 0, + "persistent": true, + "portBlocked": null, + "runtimePort": 50618 + }, + "spawnKind": "exe", + "stableUrl": "http://127.0.0.1:24680/open/fusion360", + "status": "running", + "statusVerb": "fusion_get_app_state", + "tags": [ + "fusion360", + "autodesk", + "cad", + "exports", + "aps", + "modeling" + ], + "timeouts": { + "default": 60, + "prefixes": [ + { + "prefix": "open", + "seconds": 150 + } + ], + "verbs": { + "aps_open": 150, + "aps_recent": 120, + "aps_search": 120, + "build_library_3d": 1800, + "capture_library_views": 600, + "close_all_documents": 90, + "demo": 300, + "export_3mf": 360, + "export_board_image": 90, + "export_bom": 90, + "export_cpl": 90, + "export_dwg": 150, + "export_dxf": 150, + "export_f3d": 360, + "export_fbx": 360, + "export_gerbers": 210, + "export_iges": 360, + "export_obj": 360, + "export_sat": 360, + "export_skp": 360, + "export_step": 360, + "export_stl": 360, + "export_usdz": 360, + "generate_package": 120, + "get_preferences": 25, + "install_fusion": 90, + "make_3d_package": 200, + "prewarm": 120, + "run_modeling_script": 180, + "search_cloud_files": 200, + "set_preference": 40, + "signin": 150, + "start": 90, + "walk_cloud_tree": 620 + } + }, + "updateAvailable": false, + "usageScore": 21224.36989892864, + "verbCount": 167, + "verbPrefixes": [ + "fusion_" + ], + "verbs": [ + "fusion_account", + "fusion_action_ledger", + "fusion_activate_document", + "fusion_addin_status", + "fusion_alerts", + "fusion_apply_fusion_update", + "fusion_aps_browse", + "fusion_aps_create_folder", + "fusion_aps_download", + "fusion_aps_file_info", + "fusion_aps_forget_browser", + "fusion_aps_get", + "fusion_aps_get_browser", + "fusion_aps_open", + "fusion_aps_recent", + "fusion_aps_search", + "fusion_aps_set_browser", + "fusion_aps_set_client_id", + "fusion_aps_signin", + "fusion_aps_status", + "fusion_aps_upload", + "fusion_aps_versions", + "fusion_assembly_bom", + "fusion_attach_3d_package", + "fusion_bind_library_3d", + "fusion_board_info", + "fusion_board_stackup", + "fusion_build_library_3d", + "fusion_cancel_run", + "fusion_capture_library_views", + "fusion_check_recovery", + "fusion_claim_seat", + "fusion_cleanup_cloud_files", + "fusion_close_all_documents", + "fusion_close_document", + "fusion_close_window", + "fusion_create_cloud_folder", + "fusion_create_library", + "fusion_delete_cloud_file", + "fusion_demo", + "fusion_demo_progress", + "fusion_describe", + "fusion_detect_layers", + "fusion_dismiss_blocking_dialogs", + "fusion_document_info", + "fusion_eagle_import_status", + "fusion_electron_pan", + "fusion_electron_run", + "fusion_electron_select", + "fusion_electron_zoom", + "fusion_electronics_query", + "fusion_execute_text_command", + "fusion_export_3mf", + "fusion_export_board_image", + "fusion_export_bom", + "fusion_export_cloud_file", + "fusion_export_cpl", + "fusion_export_dwg", + "fusion_export_dxf", + "fusion_export_eagle_source", + "fusion_export_f3d", + "fusion_export_fbx", + "fusion_export_gerbers", + "fusion_export_iges", + "fusion_export_lbr", + "fusion_export_obj", + "fusion_export_optimized_glb", + "fusion_export_sat", + "fusion_export_skp", + "fusion_export_source", + "fusion_export_step", + "fusion_export_stl", + "fusion_export_usdz", + "fusion_fetch_optimized_glb", + "fusion_generate_package", + "fusion_get_app_state", + "fusion_get_design_info", + "fusion_get_parameters", + "fusion_get_preferences", + "fusion_import_electronics", + "fusion_import_step", + "fusion_inspect_bodies", + "fusion_install_fusion", + "fusion_install_library_bundle", + "fusion_interactive", + "fusion_kill", + "fusion_library_manager", + "fusion_library_parts", + "fusion_list_cloud_files", + "fusion_list_cloud_projects", + "fusion_list_text_commands", + "fusion_live_feed", + "fusion_make_3d_package", + "fusion_make_showcase_project", + "fusion_make_test_project", + "fusion_managed_library_create", + "fusion_managed_library_link", + "fusion_managed_library_pull", + "fusion_managed_library_push", + "fusion_managed_library_unlink", + "fusion_mcp_call", + "fusion_mcp_enable", + "fusion_mcp_resources", + "fusion_mcp_status", + "fusion_mcp_tools", + "fusion_mech_capabilities", + "fusion_mech_operation_status", + "fusion_mechanical", + "fusion_narrate", + "fusion_new_electronics_from_eagle", + "fusion_notify_owner", + "fusion_open_board", + "fusion_open_by_urn", + "fusion_open_by_urn_note", + "fusion_open_cloud_file", + "fusion_open_design", + "fusion_open_electronics", + "fusion_open_lbr", + "fusion_open_schematic", + "fusion_physical_properties", + "fusion_plan_library_import", + "fusion_plan_route", + "fusion_preflight_lbr", + "fusion_prefs_close", + "fusion_prefs_open", + "fusion_prewarm", + "fusion_prove_result", + "fusion_readiness", + "fusion_release_seat", + "fusion_reset_workspace", + "fusion_resolve_seat_dialog", + "fusion_restart_for_addin", + "fusion_route_net", + "fusion_routing_drc", + "fusion_routing_state", + "fusion_run_modeling_script", + "fusion_save_to_cloud", + "fusion_schematic", + "fusion_screenshot_fusion", + "fusion_search_cloud_files", + "fusion_set_auto_update", + "fusion_set_design_rules", + "fusion_set_driving_badge", + "fusion_set_parameter", + "fusion_set_preference", + "fusion_set_seat_takeover", + "fusion_set_window_sizing", + "fusion_show_2d_board", + "fusion_show_3d_board", + "fusion_show_3d_package", + "fusion_show_attributes", + "fusion_show_footprint", + "fusion_show_library", + "fusion_show_project", + "fusion_show_schematic", + "fusion_show_symbol", + "fusion_signin", + "fusion_signin_2fa", + "fusion_start", + "fusion_state", + "fusion_stop", + "fusion_take_screenshot", + "fusion_uninstall", + "fusion_verb_times", + "fusion_view", + "fusion_walk_cloud_tree", + "fusion_web_signin" + ], + "version": "1.12.0-dev.8", + "versionDrift": true, + "watcher": null + }, + { + "author": "Adom Inc.", + "category": "developer-tools", + "depsInstallingSec": null, + "description": "Bridge to Hydrogen Desktop's HTTP control API. Drives setup, the Claude workspace, WSL/container lifecycle, and read-only diagnostics via direct HTTP \u2014 no shell-out, no quote mangling. HD runs as a sibling Tauri app; AD does NOT spawn it.", + "devPinned": false, + "displayName": "Hydrogen Desktop", + "docs": "https://wiki.adom.inc/adom/adom-desktop", + "entryMismatch": false, + "entrypoint": "//?/C:/Users/drew/AppData/Local/Adom Bridge/bridges/hd/", + "expectedImage": null, + "heroAvailable": false, + "heroDocs": "https://wiki.adom.inc/adom/hydrogen-desktop", + "homepage": "https://wiki.adom.inc/adom/adom-desktop", + "installPath": "\\\\?\\C:\\Users\\drew\\AppData\\Local\\Adom Bridge\\bridges\\hd", + "installSkill": "adom-wiki pkg install adom/adom-desktop", + "installedVersion": "1.0.0", + "installedVersionSource": "cache", + "instanceCount": null, + "languages": [], + "lastSpawnError": null, + "lastUsedMs": 0, + "latest": null, + "livePids": [], + "logPath": "C:/Users/drew/.adom/bridge-logs/hd.log", + "manifestSource": "updateManifestUrl", + "manifestUrl": null, + "manifestUrlFollowed": null, + "name": "hd", + "paused": false, + "persistent": false, + "platforms": { + "linux": { + "reason": "HD currently ships Windows-only; Linux variant in scope for HD's future releases.", + "supported": false + }, + "macos": { + "reason": "HD currently ships Windows-only.", + "supported": false + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": null, + "prewarm": null, + "releasedAt": "2026-06-30T13:13:22Z", + "runningImage": null, + "runningVersion": null, + "skillPkg": "adom/adom-desktop", + "source": "bundled", + "sourceDir": "//?/C:/Users/drew/AppData/Local/Adom Bridge/bridges/hd", + "spawn": { + "entrypoint": "", + "instanceCount": null, + "kind": "external-http", + "manifestPort": 47084, + "persistent": false, + "portBlocked": null, + "runtimePort": null + }, + "spawnKind": "external-http", + "stableUrl": "http://127.0.0.1:24680/open/hd", + "status": "stopped", + "statusVerb": null, + "tags": [ + "hydrogen-desktop", + "control-api", + "http", + "lifecycle" + ], + "timeouts": null, + "updateAvailable": false, + "usageScore": 0.0, + "verbCount": 11, + "verbPrefixes": [], + "verbs": [ + "hd_api", + "hd_status", + "hd_launch", + "hd_stop", + "hd_restart", + "hd_log", + "hd_screenshot", + "hd_shot", + "hd_open_url", + "hd_browser_profiles", + "hd_ship" + ], + "version": "1.0.0", + "versionDrift": false, + "watcher": null + }, + { + "author": "Adom Inc.", + "category": "EDA", + "depsInstallingSec": null, + "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", + "devPinned": false, + "displayName": "KiCad EDA", + "docs": "https://wiki.adom.inc/adom/kicad-bridge", + "entryMismatch": false, + "entrypoint": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/kicad/kicad-bridge.exe", + "expectedImage": "kicad-bridge.exe", + "heroAvailable": false, + "heroDocs": null, + "homepage": "https://wiki.adom.inc/adom/adom-bridge", + "installPath": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/kicad", + "installSkill": "adom-wiki pkg install adom/kicad-bridge", + "installedVersion": "1.0.55", + "installedVersionSource": "cache", + "instanceCount": 1, + "languages": [ + "Python", + "PowerShell" + ], + "lastSpawnError": null, + "lastUsedMs": 1790956354547, + "latest": "1.0.55", + "livePids": [ + 36872 + ], + "logPath": "C:/Users/drew/.adom/bridge-logs/kicad.log", + "manifestSource": "insidersManifestUrl", + "manifestUrl": "https://wiki.adom.inc/api/v1/pages/kicad-bridge/files/adom-bridge-kicad-manifest.json", + "manifestUrlFollowed": null, + "name": "kicad", + "paused": false, + "persistent": false, + "platforms": { + "linux": { + "reason": "", + "supported": false + }, + "macos": { + "reason": "", + "supported": false + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": 60896, + "prewarm": { + "assets": [ + "kicad-cli warm (detection)", + "KiCad first-run config + setup wizard marked done", + "KiCad's own taskbar shortcut identity" + ], + "lastRun": { + "at": "2026-10-05T15:41:58Z", + "elapsedMs": 273, + "error": null, + "ok": false, + "verb": "kicad_prewarm", + "version": "1.0.55" + }, + "requiresHostApp": true, + "timeoutSeconds": null, + "verb": "kicad_prewarm" + }, + "releasedAt": "2026-06-26T16:00:00Z", + "runningImage": "kicad-bridge.exe", + "runningVersion": "1.0.55", + "skillPkg": "adom/kicad-bridge", + "source": "cache", + "sourceDir": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/kicad", + "spawn": { + "entrypoint": "kicad-bridge.exe", + "instanceCount": 1, + "kind": "exe", + "manifestPort": 0, + "persistent": false, + "portBlocked": null, + "runtimePort": 60896 + }, + "spawnKind": "exe", + "stableUrl": "http://127.0.0.1:24680/open/kicad", + "status": "running", + "statusVerb": "kicad_status", + "tags": [ + "pcb", + "schematic", + "open-source", + "kicad-10", + "kicad-11-ready" + ], + "timeouts": { + "default": 130, + "prefixes": [], + "verbs": { + "autoroute": 900, + "demo": 600, + "enable_software_opengl": 240, + "export_gerber": 210, + "export_molecule": 420, + "export_pdf": 180, + "export_step": 300, + "export_svg": 180, + "format_upgrade": 180, + "freerouting": 900, + "open_3d_viewer": 300, + "prewarm": 120, + "run_drc": 300, + "run_erc": 300, + "upgrade": 900 + } + }, + "updateAvailable": false, + "usageScore": 1037.9305550728664, + "verbCount": 119, + "verbPrefixes": [ + "kicad_" + ], + "verbs": [ + "kicad_describe", + "kicad_status", + "kicad_readiness", + "kicad_list_versions", + "kicad_diagnostics", + "kicad_run_drc", + "kicad_run_erc", + "kicad_export_gerber", + "kicad_export_pdf", + "kicad_export_svg", + "kicad_export_step", + "kicad_export_bom_csv", + "kicad_uninstall", + "kicad_extract_netlist", + "kicad_trace_net", + "kicad_find_connections", + "kicad_analyze_connections", + "kicad_lint_board", + "kicad_lint_schematic", + "kicad_lint_library", + "kicad_format_upgrade", + "kicad_list_symbols", + "kicad_list_footprints", + "kicad_adom_library_status", + "kicad_get_settings", + "kicad_set_settings", + "kicad_list_design_rules", + "kicad_set_design_rules", + "kicad_3d_viewer_settings", + "kicad_taskbar_repair", + "kicad_prewarm", + "kicad_model_check", + "kicad_pcm_list", + "kicad_pcm_install", + "kicad_pcm_uninstall", + "kicad_pcm_add_repository", + "kicad_pcm_remove_repository", + "kicad_check_for_updates", + "kicad_upgrade", + "kicad_sch_place_symbol", + "kicad_sch_wire", + "kicad_sch_label", + "kicad_sch_move", + "kicad_sch_delete", + "kicad_sch_set_property", + "kicad_ipc_api", + "kicad_export_molecule", + "kicad_errors", + "kicad_log_tail", + "kicad_bridge_status", + "kicad_bridge_call", + "kicad_install_plugin", + "kicad_plugin_diagnose", + "kicad_install_library", + "kicad_uninstall_library", + "kicad_install_symbol", + "kicad_install_footprint", + "kicad_install_library_bundle", + "kicad_adom_library_heal", + "kicad_make_part_project", + "kicad_export_part", + "kicad_silk_fields_batch", + "kicad_silk_graphics_batch", + "kicad_text_bounds", + "kicad_silk_text_batch", + "kicad_routing_state", + "kicad_route_net", + "kicad_remove_route", + "kicad_add_zone", + "kicad_apply_zone_plan", + "kicad_zone_state", + "kicad_select_net", + "kicad_select_clear", + "kicad_remove_zone", + "kicad_routing_validate", + "kicad_board_pads", + "kicad_add_track", + "kicad_add_via", + "kicad_route", + "kicad_autoroute", + "kicad_freerouting", + "kicad_placement_state", + "kicad_move_footprint", + "kicad_placement_validate", + "kicad_refresh_board_views", + "kicad_launch", + "kicad_open_board", + "kicad_open_schematic", + "kicad_open_symbol_editor", + "kicad_open_footprint_editor", + "kicad_menu_items", + "kicad_3d_view", + "kicad_open_3d_viewer", + "kicad_open_editors", + "kicad_close", + "kicad_close_symbol_editor", + "kicad_close_footprint_editor", + "kicad_close_3d_viewer", + "kicad_dismiss_dialogs", + "kicad_window_info", + "kicad_state", + "kicad_screenshot_all", + "kicad_send_key", + "kicad_click", + "kicad_fix_keyboard", + "kicad_place_footprint", + "kicad_enable_software_opengl", + "kicad_rescan_libraries", + "kicad_show_project", + "kicad_show_symbol", + "kicad_show_footprint", + "kicad_show_3d_chip", + "kicad_show_schematic", + "kicad_show_2d_board", + "kicad_show_3d_board", + "kicad_show_library", + "kicad_progress", + "kicad_verb_times", + "kicad_demo" + ], + "version": "1.0.55", + "versionDrift": false, + "watcher": { + "configKey": "project_watch", + "defaultGlobs": [ + "*.kicad_pcb", + "*.kicad_sch", + "*.kicad_pro" + ], + "description": "Monitors a KiCad project folder for file changes and syncs them to Docker. Configured via the set_project_watch / trigger_project_watch verbs (AD core owns the watcher; the KiCad section of AD's Settings panel was removed in AD 1.9.117 \u2014 the config keys are still honored).", + "displayName": "Project Watch", + "supports": true + } + }, + { + "author": "Adom Inc.", + "category": "Browser Automation", + "depsInstallingSec": null, + "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", + "devPinned": false, + "displayName": "Adom Native Browser", + "docs": "https://wiki.adom.inc/adom/nb-bridge", + "entryMismatch": false, + "entrypoint": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/native-browser/server.js", + "expectedImage": "node.exe", + "heroAvailable": false, + "heroDocs": null, + "homepage": "https://wiki.adom.inc/adom/nb-bridge", + "installPath": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/native-browser", + "installSkill": "adom-wiki pkg install adom/nb-bridge", + "installedVersion": "0.16.33", + "installedVersionSource": "cache", + "instanceCount": 1, + "languages": [ + "Node/JS" + ], + "lastSpawnError": null, + "lastUsedMs": 1791220671752, + "latest": "0.16.33", + "livePids": [ + 32228 + ], + "logPath": "C:/Users/drew/.adom/bridge-logs/native-browser.log", + "manifestSource": "insidersManifestUrl", + "manifestUrl": "https://wiki.adom.inc/api/pages/adom/nb-bridge/files/native-browser-bridge-manifest.json", + "manifestUrlFollowed": null, + "name": "native-browser", + "paused": false, + "persistent": true, + "platforms": { + "linux": { + "reason": "Pending; bridge + host are OS-agnostic Node, only the install/registration layer is Windows-specific.", + "supported": false + }, + "macos": { + "reason": "Windows-first; Kyle ports to macOS as a fast-follow (NativeMessagingHosts live under ~/Library/Application Support//NativeMessagingHosts; bridge/host are OS-agnostic Node).", + "supported": false + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": 51703, + "prewarm": { + "assets": [], + "lastRun": { + "at": "2026-09-22T19:44:55Z", + "elapsedMs": 41, + "error": null, + "ok": true, + "verb": "nbrowser_prewarm", + "version": "0.16.33" + }, + "requiresHostApp": false, + "timeoutSeconds": null, + "verb": "nbrowser_prewarm" + }, + "releasedAt": "2026-09-22T15:18:44Z", + "runningImage": "node.exe", + "runningVersion": "0.16.33", + "skillPkg": "adom/nb-bridge", + "source": "cache", + "sourceDir": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/native-browser", + "spawn": { + "entrypoint": "server.js", + "instanceCount": 1, + "kind": "node", + "manifestPort": 0, + "persistent": true, + "portBlocked": null, + "runtimePort": 51703 + }, + "spawnKind": "node", + "stableUrl": "http://127.0.0.1:24680/open/native-browser", + "status": "running", + "statusVerb": "nbrowser_status", + "tags": [ + "nb", + "nbe", + "native-browser", + "chrome", + "edge", + "nbrowser", + "pup-parity" + ], + "timeouts": { + "default": 60, + "prefixes": [ + { + "prefix": "screenshot", + "seconds": 120 + }, + { + "prefix": "record", + "seconds": 120 + } + ], + "verbs": { + "fetch_url": 120, + "input_dispatch": 120, + "select": 30, + "wait": 125 + } + }, + "updateAvailable": false, + "usageScore": 917.1856854627648, + "verbCount": 88, + "verbPrefixes": [ + "nbrowser_" + ], + "verbs": [ + "nbrowser_status", + "nbrowser_readiness", + "nbrowser_install_chrome", + "nbrowser_prewarm", + "nbrowser_chrome_status", + "nbrowser_ping", + "nbrowser_progress", + "nbrowser_activity", + "nbrowser_open_window", + "nbrowser_close_window", + "nbrowser_owned", + "nbrowser_cleanup", + "nbrowser_force_close", + "nbrowser_switch_window", + "nbrowser_list_windows", + "nbrowser_window_state", + "nbrowser_fullscreen", + "nbrowser_login_state", + "nbrowser_credentials", + "nbrowser_profiles", + "nbrowser_use_profile", + "nbrowser_pin_session", + "nbrowser_open_os_window", + "nbrowser_os_windows", + "nbrowser_wake_profile", + "nbrowser_resolve_profiles", + "nbrowser_whoami", + "nbrowser_profile_block", + "nbrowser_profile_unblock", + "nbrowser_rescan", + "nbrowser_resync_sessions", + "nbrowser_open_tab", + "nbrowser_close_tab", + "nbrowser_switch_tab", + "nbrowser_list_tabs", + "nbrowser_navigate", + "nbrowser_reload", + "nbrowser_back", + "nbrowser_forward", + "nbrowser_wait", + "nbrowser_wait_for", + "nbrowser_keepalive", + "nbrowser_screenshot", + "nbrowser_screenshot_full_res", + "nbrowser_eval", + "nbrowser_evaluate", + "nbrowser_input_dispatch", + "nbrowser_scroll", + "nbrowser_click", + "nbrowser_select", + "nbrowser_set_file_input", + "nbrowser_set_date", + "nbrowser_totp", + "nbrowser_register_totp", + "nbrowser_add_extension_origin", + "nbrowser_type", + "nbrowser_press_key", + "nbrowser_fetch_url", + "nbrowser_download_wait", + "nbrowser_errors", + "nbrowser_close", + "nbrowser_record_start", + "nbrowser_record_stop", + "nbrowser_record_status", + "nbrowser_record_list", + "nbrowser_flash", + "nbrowser_flash_stop", + "nbrowser_apply_settings", + "nbrowser_settings", + "nbrowser_flash_mode", + "nbrowser_taskbar", + "nbrowser_describe", + "nbrowser_handle_dialog", + "nbrowser_state", + "nbrowser_demo", + "nbrowser_uninstall", + "nbrowser_dev_reload", + "nbrowser_clear_cookies", + "nbrowser_caption", + "nbrowser_focus", + "nbrowser_hover", + "nbrowser_double_click", + "nbrowser_right_click", + "nbrowser_login_form", + "nbrowser_autologin", + "nbrowser_cursor", + "nbrowser_events", + "nbrowser_download" + ], + "version": "0.16.33", + "versionDrift": false, + "watcher": null + }, + { + "author": "Adom Inc.", + "category": "Manufacturing", + "depsInstallingSec": null, + "description": "The Adom Desktop bridge for the Essemtec Puma / ePlace. Reads + dry-run: machine state, recipe/package/component/feeder reads, the full feeder fleet from Supply/Feeder/*.feeder.xml, and dry-run of any WriteOp (preflight + physics-bounds + unified diff). F4 adds puma_write \u2014 GATED production writes of FileWrite.* library objects to real XML files under --data-folder (atomic write + XML-verify + audit; approver enforced; refused when the machine is Productive), plus puma_revert \u2014 canonical revert of a prior write by correlationId (CAS-pinned to the recorded result hash, its own audit entry, same write gate). Off by default: requires --enable-writes. eData stays vendor-read-only. 0.2.5: puma_feeders binds each loaded lane to its component/part number (carrierType, componentName, partValue, packageName, componentBarcode) via the Supply/Carrier -> Component library chain (#639). 0.2.6: the write audit records ab's X-Adom-Verified-Origin (relay | local | bridge, plus the X-Adom-Caller-Delegate chain for a bridge callback; a missing header records as local-unattributed) beside the approver \u2014 attribution, never a gate. New puma_write_status (declared as statusVerb): what happened to a write by correlationId, so a caller whose transport timed out polls instead of declaring the write unknown. 0.2.7: the host is a GUI-subsystem exe (no console window of its own) and its git subprocess runs with CreateNoWindow, so the bridge can never leave a conhost window on the desktop across ab respawns or reinstalls.", + "devPinned": false, + "displayName": "Puma Bridge (Essemtec ePlace)", + "docs": "https://wiki.adom.inc/adom/puma-bridge", + "entryMismatch": false, + "entrypoint": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puma-bridge/puma-bridge-host.exe", + "expectedImage": "puma-bridge-host.exe", + "heroAvailable": false, + "heroDocs": null, + "homepage": "https://wiki.adom.inc/adom/puma-bridge", + "installPath": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puma-bridge", + "installSkill": "adom-wiki pkg install adom/puma-bridge", + "installedVersion": "0.2.7", + "installedVersionSource": "cache", + "instanceCount": 1, + "languages": [ + "Binary" + ], + "lastSpawnError": null, + "lastUsedMs": 1790623763822, + "latest": "0.2.6", + "livePids": [ + 32008 + ], + "logPath": "C:/Users/drew/.adom/bridge-logs/puma-bridge.log", + "manifestSource": "updateManifestUrl", + "manifestUrl": "https://wiki.adom.inc/api/v1/pages/adom/puma-bridge/files/adom-bridge-puma-manifest.json", + "manifestUrlFollowed": null, + "name": "puma-bridge", + "paused": false, + "persistent": true, + "platforms": { + "linux": { + "reason": "", + "supported": true + }, + "macos": { + "reason": "", + "supported": false + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": 64764, + "prewarm": null, + "releasedAt": "2026-09-23T15:54:11Z", + "runningImage": "puma-bridge-host.exe", + "runningVersion": "0.2.7", + "skillPkg": "adom/puma-bridge", + "source": "cache", + "sourceDir": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puma-bridge", + "spawn": { + "entrypoint": "puma-bridge-host.exe", + "instanceCount": 1, + "kind": "exe", + "manifestPort": 0, + "persistent": true, + "portBlocked": null, + "runtimePort": 64764 + }, + "spawnKind": "exe", + "stableUrl": "http://127.0.0.1:24680/open/puma-bridge", + "status": "running", + "statusVerb": "puma_write_status", + "tags": [ + "smt", + "essemtec", + "puma", + "eplace", + "pick-and-place", + "manufacturing", + "factoryos" + ], + "timeouts": null, + "updateAvailable": false, + "usageScore": 2.691439652959016, + "verbCount": 9, + "verbPrefixes": [ + "puma_" + ], + "verbs": [ + "puma_ping", + "puma_state", + "puma_get", + "puma_feeders", + "puma_dryrun", + "puma_write", + "puma_revert", + "puma_write_status", + "puma_describe" + ], + "version": "0.2.7", + "versionDrift": false, + "watcher": null + }, + { + "author": "Adom Inc.", + "category": "Browser Automation", + "depsInstallingSec": null, + "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", + "devPinned": false, + "displayName": "Puppeteer (Chrome for Testing + Native)", + "docs": "https://wiki.adom.inc/adom/pup-bridge", + "entryMismatch": false, + "entrypoint": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puppeteer/adom-pup-bridge.exe", + "expectedImage": "adom-pup-bridge.exe", + "heroAvailable": false, + "heroDocs": null, + "homepage": "https://wiki.adom.inc/adom/adom-bridge", + "installPath": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puppeteer", + "installSkill": "adom-wiki pkg install adom/pup-bridge", + "installedVersion": "2.0.697", + "installedVersionSource": "cache", + "instanceCount": 1, + "languages": [ + "Binary" + ], + "lastSpawnError": null, + "lastUsedMs": 1791212202067, + "latest": "2.0.697", + "livePids": [ + 68812 + ], + "logPath": "C:/Users/drew/.adom/bridge-logs/puppeteer.log", + "manifestSource": "insidersManifestUrl", + "manifestUrl": "https://wiki.adom.inc/api/v1/pages/pup-bridge/files/adom-bridge-puppeteer-manifest.json", + "manifestUrlFollowed": null, + "name": "puppeteer", + "paused": false, + "persistent": true, + "platforms": { + "linux": { + "reason": "not yet", + "supported": false + }, + "macos": { + "reason": "The Rust exe is Windows-only for now; host-mac is stubbed for the Mac port. macOS runs the Node build (public tier).", + "supported": false + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": 57134, + "prewarm": { + "assets": [ + "chrome-for-testing" + ], + "lastRun": { + "at": "2026-10-05T14:55:58Z", + "elapsedMs": 1409, + "error": null, + "ok": true, + "verb": "pup_prewarm", + "version": "2.0.697" + }, + "requiresHostApp": false, + "timeoutSeconds": 900, + "verb": "pup_prewarm" + }, + "releasedAt": "2026-10-05T14:55:48Z", + "runningImage": "adom-pup-bridge.exe", + "runningVersion": "2.0.697", + "skillPkg": "adom/pup-bridge", + "source": "cache", + "sourceDir": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/puppeteer", + "spawn": { + "entrypoint": "adom-pup-bridge.exe", + "instanceCount": 1, + "kind": "exe", + "manifestPort": 8851, + "persistent": true, + "portBlocked": null, + "runtimePort": 57134 + }, + "spawnKind": "exe", + "stableUrl": "http://127.0.0.1:24680/open/puppeteer", + "status": "running", + "statusVerb": "pup_readiness", + "tags": [ + "puppeteer", + "chrome", + "headful", + "recording" + ], + "timeouts": { + "default": 60, + "prefixes": [ + { + "prefix": "record", + "seconds": 90 + } + ], + "verbs": { + "credential_reveal": 150, + "fill_card": 150, + "open_window": 150, + "prewarm": 240, + "use": 240 + } + }, + "updateAvailable": false, + "usageScore": 580.1076328182536, + "verbCount": 97, + "verbPrefixes": [ + "pup_", + "browser_" + ], + "verbs": [ + "pup_describe", + "pup_readiness", + "pup_prewarm", + "pup_status", + "pup_list_windows", + "pup_my_windows", + "pup_list_sessions", + "pup_open_window", + "pup_navigate", + "pup_eval", + "pup_evaluate", + "pup_screenshot", + "pup_close_window", + "pup_close", + "pup_close_all_windows", + "pup_window_info", + "pup_open_tab", + "pup_list_tabs", + "pup_switch_tab", + "pup_close_tab", + "pup_switch_window", + "pup_click", + "pup_select", + "pup_hover", + "pup_type", + "pup_press_key", + "pup_scroll", + "pup_fill", + "pup_wait", + "pup_reload", + "pup_back", + "pup_forward", + "pup_set_viewport", + "pup_errors", + "pup_dialog", + "pup_input_dispatch", + "pup_render_html", + "pup_fetch_url", + "pup_maximize", + "pup_raise_os_window", + "pup_focus_window", + "pup_lower_os_window", + "pup_alert_window", + "pup_progress", + "pup_configure", + "pup_use", + "pup_clear_cookies", + "pup_clear_site_data", + "pup_quit_idle_browsers", + "pup_rescan", + "pup_restart_browser", + "pup_set_identity", + "pup_audit_icons", + "pup_dashboard", + "pup_credentials", + "credential_list", + "credential_set", + "credential_delete", + "pup_login", + "pup_google_accounts", + "pup_google_signin", + "pup_credential_reveal", + "pup_card_set", + "pup_cards", + "pup_card_delete", + "pup_fill_card", + "pup_wiki_set_view", + "pup_annotate", + "pup_floaty_click", + "pup_record_start", + "pup_record_stop", + "pup_record_status", + "pup_record_list", + "pup_recorder_open", + "pup_recorder_close", + "pup_screen_record_start", + "pup_screen_record_stop", + "pup_screen_record_status", + "pup_screen_record_list", + "pup_highfps_extend", + "pup_screenshot_element", + "pup_screenshot_full_res", + "pup_download", + "pup_launch", + "pup_deps", + "pup_install_toolbar", + "pup_reap_profiles", + "pup_cache_prune", + "pup_sweep_identities", + "pup_uninstall", + "pup_move_to_workspace", + "pup_assisted_click", + "pup_import_scan", + "pup_import_browser_logins", + "pup_vault_sync", + "pup_vault_export", + "pup_vault_import" + ], + "version": "2.0.697", + "versionDrift": false, + "watcher": null + }, + { + "author": "Adom Inc.", + "category": "MCAD", + "depsInstallingSec": null, + "description": "Bridge for SOLIDWORKS desktop CAD \u2014 create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.", + "devPinned": false, + "displayName": "SOLIDWORKS", + "docs": "https://wiki.adom.inc/adom/solidworks-bridge", + "entryMismatch": false, + "entrypoint": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/solidworks/swx-bridge.exe", + "expectedImage": "swx-bridge.exe", + "heroAvailable": false, + "heroDocs": null, + "homepage": "https://wiki.adom.inc/adom/adom-bridge", + "installPath": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/solidworks", + "installSkill": "adom-wiki pkg install adom/solidworks-bridge", + "installedVersion": "0.9.21", + "installedVersionSource": "cache", + "instanceCount": 0, + "languages": [ + "Binary" + ], + "lastSpawnError": null, + "lastUsedMs": 1791224978746, + "latest": "0.9.19", + "livePids": [], + "logPath": "C:/Users/drew/.adom/bridge-logs/solidworks.log", + "manifestSource": "updateManifestUrl", + "manifestUrl": "https://wiki.adom.inc/api/v1/pages/solidworks-bridge/files/adom-bridge-solidworks-manifest.json", + "manifestUrlFollowed": null, + "name": "solidworks", + "paused": false, + "persistent": false, + "platforms": { + "linux": { + "reason": "SOLIDWORKS desktop is Windows-only; the bridge drives its COM API.", + "supported": false + }, + "macos": { + "reason": "SOLIDWORKS desktop is Windows-only; the bridge drives its COM API.", + "supported": false + }, + "windows": { + "reason": "", + "supported": true + } + }, + "port": 49861, + "prewarm": null, + "releasedAt": "2026-10-05T18:49:46Z", + "runningImage": null, + "runningVersion": "0.9.19", + "skillPkg": "adom/solidworks-bridge", + "source": "cache", + "sourceDir": "C:/Users/drew/AppData/Local/Adom Bridge/bridges-cache/solidworks", + "spawn": { + "entrypoint": "swx-bridge.exe", + "instanceCount": 0, + "kind": "exe", + "manifestPort": 0, + "persistent": false, + "portBlocked": null, + "runtimePort": 49861 + }, + "spawnKind": "exe", + "stableUrl": "http://127.0.0.1:24680/open/solidworks", + "status": "running", + "statusVerb": "swx_status", + "tags": [ + "solidworks", + "mcad", + "cad", + "bom", + "step", + "mass-properties", + "modeling", + "fusion-parity" + ], + "timeouts": { + "default": 120, + "prefixes": [], + "verbs": { + "action_ledger": 20, + "activate_document": 180, + "bom": 300, + "check_recovery": 180, + "close_all_documents": 240, + "close_all_safely": 600, + "close_doc": 180, + "close_import_temps": 600, + "close_window": 120, + "cloud_status": 90, + "design_info": 180, + "dismiss_blocking_dialogs": 180, + "document_info": 120, + "export": 300, + "export_3mf": 300, + "export_dwg": 300, + "export_dxf": 300, + "export_glb": 300, + "export_iges": 300, + "export_parasolid": 300, + "export_pdf": 300, + "export_sat": 300, + "export_step": 300, + "export_stl": 300, + "find_files": 180, + "get_app_state": 90, + "get_parameters": 240, + "get_preferences": 90, + "import": 420, + "inspect_bodies": 180, + "interference": 600, + "kill": 240, + "new_document": 180, + "open": 300, + "pack_and_go": 600, + "rebuild_check": 300, + "render_image": 600, + "reset_workspace": 600, + "run_script": 420, + "save_as": 420, + "set_driving_badge": 120, + "set_parameter": 420, + "set_preference": 90, + "show_assembly": 600, + "show_drawing": 600, + "show_part": 600, + "shutdown": 900, + "sign_in": 240, + "start": 300, + "state": 180, + "stop": 420, + "verb_times": 20, + "window_info": 120 + } + }, + "updateAvailable": false, + "usageScore": 561.3708117917515, + "verbCount": 69, + "verbPrefixes": [ + "swx_" + ], + "verbs": [ + "swx_action_ledger", + "swx_activate_document", + "swx_add_component", + "swx_bom", + "swx_check_recovery", + "swx_close_all_documents", + "swx_close_all_safely", + "swx_close_doc", + "swx_close_import_temps", + "swx_close_window", + "swx_cloud_status", + "swx_com_eval", + "swx_describe", + "swx_design_info", + "swx_dismiss_blocking_dialogs", + "swx_docs", + "swx_document_info", + "swx_export", + "swx_export_3mf", + "swx_export_dwg", + "swx_export_dxf", + "swx_export_glb", + "swx_export_iges", + "swx_export_parasolid", + "swx_export_pdf", + "swx_export_sat", + "swx_export_step", + "swx_export_stl", + "swx_find_files", + "swx_get_app_state", + "swx_get_parameters", + "swx_get_preferences", + "swx_import", + "swx_inspect_bodies", + "swx_interference", + "swx_kill", + "swx_mass_properties", + "swx_new_document", + "swx_open", + "swx_pack_and_go", + "swx_progress", + "swx_props_get", + "swx_props_set", + "swx_readiness", + "swx_rebuild_check", + "swx_recover_list", + "swx_render_image", + "swx_reset_workspace", + "swx_run_macro", + "swx_run_script", + "swx_save", + "swx_save_as", + "swx_screenshot", + "swx_set_driving_badge", + "swx_set_parameter", + "swx_set_preference", + "swx_show_assembly", + "swx_show_drawing", + "swx_show_part", + "swx_shutdown", + "swx_sign_in", + "swx_start", + "swx_state", + "swx_status", + "swx_stop", + "swx_uninstall", + "swx_user_presence", + "swx_verb_times", + "swx_window_info" + ], + "version": "0.9.21", + "versionDrift": true, + "watcher": null + } + ], + "current_os": "windows", + "durationMs": 23, + "error": "", + "invalidCount": 0, + "ok": true, + "output": "", + "schemaVersion": 1, + "staleCheckedAt": 1791215191, + "status": "ok", + "success": true, + "summary": { + "bundled": 1, + "cache": 6, + "invalid": 0, + "paused": 0, + "third_party": 0, + "total": 7 + }, + "timeoutSeconds": 10 + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/calls/0036-run_script.json b/docs/mechanical-verification/cp9-readonly/calls/0036-run_script.json new file mode 100644 index 0000000..6ccf5b7 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/calls/0036-run_script.json @@ -0,0 +1,31 @@ +{ + "target": "adom_drew2", + "verb": "run_script", + "args": { + "interpreter": "powershell", + "scriptB64": "CiRFcnJvckFjdGlvblByZWZlcmVuY2UgPSAnU3RvcCcKdHJ5IHsKICAkcmFscGhFeHBlY3RlZCA9IFtUZXh0LkVuY29kaW5nXTo6VVRGOC5HZXRTdHJpbmcoW0NvbnZlcnRdOjpGcm9tQmFzZTY0U3RyaW5nKCdleUpvYjNOMElqb2dJa0ZFVDAxZlJGSkZWeklpTENBaWNHbGtJam9nTkRJM09EQXNJQ0p3YjNKMElqb2dOVEEyTVRnc0lDSmxlR1ZqZFhSaFlteGxVR0YwYUNJNklDSkRPbHhjVlhObGNuTmNYR1J5WlhkY1hFRndjRVJoZEdGY1hFeHZZMkZzWEZ4QlpHOXRJRUp5YVdSblpWeGNZbkpwWkdkbGN5MWpZV05vWlZ4Y1puVnphVzl1TXpZd1hGeGhaRzl0TFdaMWMybHZiaTFpY21sa1oyVXVaWGhsSWl3Z0ltbHRZV2RsSWpvZ0ltRmtiMjB0Wm5WemFXOXVMV0p5YVdSblpTNWxlR1VpZlE9PScpKSB8IENvbnZlcnRGcm9tLUpzb24KICAkcmFscGhQb3J0ID0gW2ludF0kcmFscGhFeHBlY3RlZC5wb3J0CiAgJHJhbHBoT3duZXIgPSBbaW50XSRyYWxwaEV4cGVjdGVkLnBpZAogIGlmIChbRW52aXJvbm1lbnRdOjpNYWNoaW5lTmFtZSAtaW5lIFtzdHJpbmddJHJhbHBoRXhwZWN0ZWQuaG9zdCkgeyB0aHJvdyAnTmF0aXZlIGhvc3QgaWRlbnRpdHkgZGlmZmVycycgfQogICRyYWxwaFByb2Nlc3MgPSBHZXQtQ2ltSW5zdGFuY2UgV2luMzJfUHJvY2VzcyAtRmlsdGVyICgnUHJvY2Vzc0lkPScgKyAkcmFscGhPd25lcikKICBpZiAoLW5vdCAkcmFscGhQcm9jZXNzIC1vciAkcmFscGhQcm9jZXNzLk5hbWUgLWluZSBbc3RyaW5nXSRyYWxwaEV4cGVjdGVkLmltYWdlKSB7IHRocm93ICdJbnZlbnRvcnkgUElEL2ltYWdlIGlzIG5vdCB0aGUgbmF0aXZlIGJyaWRnZScgfQogIGlmIChbSU8uUGF0aF06OkdldEZ1bGxQYXRoKCRyYWxwaFByb2Nlc3MuRXhlY3V0YWJsZVBhdGgpIC1pbmUgW0lPLlBhdGhdOjpHZXRGdWxsUGF0aChbc3RyaW5nXSRyYWxwaEV4cGVjdGVkLmV4ZWN1dGFibGVQYXRoKSkgeyB0aHJvdyAnTmF0aXZlIGV4ZWN1dGFibGUgcGF0aCBkaWZmZXJzIGZyb20gaW52ZW50b3J5JyB9CiAgJHJhbHBoTGlzdGVuZXJzID0gQChHZXQtTmV0VENQQ29ubmVjdGlvbiAtU3RhdGUgTGlzdGVuIC1Mb2NhbFBvcnQgJHJhbHBoUG9ydCkKICBpZiAoJHJhbHBoTGlzdGVuZXJzLkNvdW50IC1lcSAwKSB7IHRocm93ICdEaXNjb3ZlcmVkIHBvcnQgaGFzIG5vIGxpc3RlbmVyJyB9CiAgZm9yZWFjaCAoJHJhbHBoTGlzdGVuZXIgaW4gJHJhbHBoTGlzdGVuZXJzKSB7CiAgICBpZiAoJHJhbHBoTGlzdGVuZXIuT3duaW5nUHJvY2VzcyAtbmUgJHJhbHBoT3duZXIgLW9yICRyYWxwaExpc3RlbmVyLkxvY2FsQWRkcmVzcyAtbm90aW4gQCgnMTI3LjAuMC4xJywnOjoxJykpIHsgdGhyb3cgJ0xpc3RlbmVyIGlzIG5vdCBvd25lZCBieSB0aGUgZXhwZWN0ZWQgbG9vcGJhY2sgYnJpZGdlJyB9CiAgfQogICRyYWxwaEhlYWx0aCA9IEludm9rZS1SZXN0TWV0aG9kIC1VcmkgKCdodHRwOi8vMTI3LjAuMC4xOicgKyAkcmFscGhQb3J0ICsgJy9oZWFsdGgnKSAtTWV0aG9kIEdldCAtVGltZW91dFNlYyA4CiAgJHJhbHBoQWdhaW4gPSBHZXQtQ2ltSW5zdGFuY2UgV2luMzJfUHJvY2VzcyAtRmlsdGVyICgnUHJvY2Vzc0lkPScgKyAkcmFscGhPd25lcikKICBpZiAoLW5vdCAkcmFscGhBZ2FpbiAtb3IgJHJhbHBoQWdhaW4uQ3JlYXRpb25EYXRlIC1uZSAkcmFscGhQcm9jZXNzLkNyZWF0aW9uRGF0ZSAtb3IgJHJhbHBoQWdhaW4uRXhlY3V0YWJsZVBhdGggLWluZSAkcmFscGhQcm9jZXNzLkV4ZWN1dGFibGVQYXRoKSB7IHRocm93ICdOYXRpdmUgcHJvY2VzcyBjaGFuZ2VkIGR1cmluZyBoZWFsdGggb2JzZXJ2YXRpb24nIH0KICAkcmFscGhMaXN0ZW5lcnNBZnRlciA9IEAoR2V0LU5ldFRDUENvbm5lY3Rpb24gLVN0YXRlIExpc3RlbiAtTG9jYWxQb3J0ICRyYWxwaFBvcnQpCiAgaWYgKCRyYWxwaExpc3RlbmVyc0FmdGVyLkNvdW50IC1lcSAwKSB7IHRocm93ICdMaXN0ZW5lciBkaXNhcHBlYXJlZCBkdXJpbmcgaGVhbHRoIG9ic2VydmF0aW9uJyB9CiAgZm9yZWFjaCAoJHJhbHBoTGlzdGVuZXIgaW4gJHJhbHBoTGlzdGVuZXJzQWZ0ZXIpIHsKICAgIGlmICgkcmFscGhMaXN0ZW5lci5Pd25pbmdQcm9jZXNzIC1uZSAkcmFscGhPd25lciAtb3IgJHJhbHBoTGlzdGVuZXIuTG9jYWxBZGRyZXNzIC1ub3RpbiBAKCcxMjcuMC4wLjEnLCc6OjEnKSkgeyB0aHJvdyAnTGlzdGVuZXIgY2hhbmdlZCBkdXJpbmcgaGVhbHRoIG9ic2VydmF0aW9uJyB9CiAgfQogIEB7IGhvc3Q9W0Vudmlyb25tZW50XTo6TWFjaGluZU5hbWU7IHBpZD0kcmFscGhPd25lcjsgcG9ydD0kcmFscGhQb3J0OyBleGVjdXRhYmxlUGF0aD0kcmFscGhQcm9jZXNzLkV4ZWN1dGFibGVQYXRoOwogICAgIGltYWdlPSRyYWxwaFByb2Nlc3MuTmFtZTsgcHJvY2Vzc0NyZWF0ZWRVdGM9JHJhbHBoUHJvY2Vzcy5DcmVhdGlvbkRhdGUuVG9Vbml2ZXJzYWxUaW1lKCkuVG9TdHJpbmcoJ28nKTsKICAgICBsaXN0ZW5lckFkZHJlc3Nlcz1AKCRyYWxwaExpc3RlbmVyc0FmdGVyIHwgRm9yRWFjaC1PYmplY3QgeyAkXy5Mb2NhbEFkZHJlc3MgfSk7CiAgICAgc3RhYmxlPSR0cnVlOyBoZWFsdGg9JHJhbHBoSGVhbHRoIH0gfCBDb252ZXJ0VG8tSnNvbiAtRGVwdGggMTIgLUNvbXByZXNzCiAgZXhpdCAwCn0gY2F0Y2ggewogIFtDb25zb2xlXTo6RXJyb3IuV3JpdGVMaW5lKCRfLkV4Y2VwdGlvbi5NZXNzYWdlKQogIGV4aXQgMQp9Cg==", + "timeoutSeconds": 30, + "reason": "Read the discovered Fusion bridge process and its loopback health version without changing runtime state" + }, + "requestJson": "{\"interpreter\": \"powershell\", \"scriptB64\": \"CiRFcnJvckFjdGlvblByZWZlcmVuY2UgPSAnU3RvcCcKdHJ5IHsKICAkcmFscGhFeHBlY3RlZCA9IFtUZXh0LkVuY29kaW5nXTo6VVRGOC5HZXRTdHJpbmcoW0NvbnZlcnRdOjpGcm9tQmFzZTY0U3RyaW5nKCdleUpvYjNOMElqb2dJa0ZFVDAxZlJGSkZWeklpTENBaWNHbGtJam9nTkRJM09EQXNJQ0p3YjNKMElqb2dOVEEyTVRnc0lDSmxlR1ZqZFhSaFlteGxVR0YwYUNJNklDSkRPbHhjVlhObGNuTmNYR1J5WlhkY1hFRndjRVJoZEdGY1hFeHZZMkZzWEZ4QlpHOXRJRUp5YVdSblpWeGNZbkpwWkdkbGN5MWpZV05vWlZ4Y1puVnphVzl1TXpZd1hGeGhaRzl0TFdaMWMybHZiaTFpY21sa1oyVXVaWGhsSWl3Z0ltbHRZV2RsSWpvZ0ltRmtiMjB0Wm5WemFXOXVMV0p5YVdSblpTNWxlR1VpZlE9PScpKSB8IENvbnZlcnRGcm9tLUpzb24KICAkcmFscGhQb3J0ID0gW2ludF0kcmFscGhFeHBlY3RlZC5wb3J0CiAgJHJhbHBoT3duZXIgPSBbaW50XSRyYWxwaEV4cGVjdGVkLnBpZAogIGlmIChbRW52aXJvbm1lbnRdOjpNYWNoaW5lTmFtZSAtaW5lIFtzdHJpbmddJHJhbHBoRXhwZWN0ZWQuaG9zdCkgeyB0aHJvdyAnTmF0aXZlIGhvc3QgaWRlbnRpdHkgZGlmZmVycycgfQogICRyYWxwaFByb2Nlc3MgPSBHZXQtQ2ltSW5zdGFuY2UgV2luMzJfUHJvY2VzcyAtRmlsdGVyICgnUHJvY2Vzc0lkPScgKyAkcmFscGhPd25lcikKICBpZiAoLW5vdCAkcmFscGhQcm9jZXNzIC1vciAkcmFscGhQcm9jZXNzLk5hbWUgLWluZSBbc3RyaW5nXSRyYWxwaEV4cGVjdGVkLmltYWdlKSB7IHRocm93ICdJbnZlbnRvcnkgUElEL2ltYWdlIGlzIG5vdCB0aGUgbmF0aXZlIGJyaWRnZScgfQogIGlmIChbSU8uUGF0aF06OkdldEZ1bGxQYXRoKCRyYWxwaFByb2Nlc3MuRXhlY3V0YWJsZVBhdGgpIC1pbmUgW0lPLlBhdGhdOjpHZXRGdWxsUGF0aChbc3RyaW5nXSRyYWxwaEV4cGVjdGVkLmV4ZWN1dGFibGVQYXRoKSkgeyB0aHJvdyAnTmF0aXZlIGV4ZWN1dGFibGUgcGF0aCBkaWZmZXJzIGZyb20gaW52ZW50b3J5JyB9CiAgJHJhbHBoTGlzdGVuZXJzID0gQChHZXQtTmV0VENQQ29ubmVjdGlvbiAtU3RhdGUgTGlzdGVuIC1Mb2NhbFBvcnQgJHJhbHBoUG9ydCkKICBpZiAoJHJhbHBoTGlzdGVuZXJzLkNvdW50IC1lcSAwKSB7IHRocm93ICdEaXNjb3ZlcmVkIHBvcnQgaGFzIG5vIGxpc3RlbmVyJyB9CiAgZm9yZWFjaCAoJHJhbHBoTGlzdGVuZXIgaW4gJHJhbHBoTGlzdGVuZXJzKSB7CiAgICBpZiAoJHJhbHBoTGlzdGVuZXIuT3duaW5nUHJvY2VzcyAtbmUgJHJhbHBoT3duZXIgLW9yICRyYWxwaExpc3RlbmVyLkxvY2FsQWRkcmVzcyAtbm90aW4gQCgnMTI3LjAuMC4xJywnOjoxJykpIHsgdGhyb3cgJ0xpc3RlbmVyIGlzIG5vdCBvd25lZCBieSB0aGUgZXhwZWN0ZWQgbG9vcGJhY2sgYnJpZGdlJyB9CiAgfQogICRyYWxwaEhlYWx0aCA9IEludm9rZS1SZXN0TWV0aG9kIC1VcmkgKCdodHRwOi8vMTI3LjAuMC4xOicgKyAkcmFscGhQb3J0ICsgJy9oZWFsdGgnKSAtTWV0aG9kIEdldCAtVGltZW91dFNlYyA4CiAgJHJhbHBoQWdhaW4gPSBHZXQtQ2ltSW5zdGFuY2UgV2luMzJfUHJvY2VzcyAtRmlsdGVyICgnUHJvY2Vzc0lkPScgKyAkcmFscGhPd25lcikKICBpZiAoLW5vdCAkcmFscGhBZ2FpbiAtb3IgJHJhbHBoQWdhaW4uQ3JlYXRpb25EYXRlIC1uZSAkcmFscGhQcm9jZXNzLkNyZWF0aW9uRGF0ZSAtb3IgJHJhbHBoQWdhaW4uRXhlY3V0YWJsZVBhdGggLWluZSAkcmFscGhQcm9jZXNzLkV4ZWN1dGFibGVQYXRoKSB7IHRocm93ICdOYXRpdmUgcHJvY2VzcyBjaGFuZ2VkIGR1cmluZyBoZWFsdGggb2JzZXJ2YXRpb24nIH0KICAkcmFscGhMaXN0ZW5lcnNBZnRlciA9IEAoR2V0LU5ldFRDUENvbm5lY3Rpb24gLVN0YXRlIExpc3RlbiAtTG9jYWxQb3J0ICRyYWxwaFBvcnQpCiAgaWYgKCRyYWxwaExpc3RlbmVyc0FmdGVyLkNvdW50IC1lcSAwKSB7IHRocm93ICdMaXN0ZW5lciBkaXNhcHBlYXJlZCBkdXJpbmcgaGVhbHRoIG9ic2VydmF0aW9uJyB9CiAgZm9yZWFjaCAoJHJhbHBoTGlzdGVuZXIgaW4gJHJhbHBoTGlzdGVuZXJzQWZ0ZXIpIHsKICAgIGlmICgkcmFscGhMaXN0ZW5lci5Pd25pbmdQcm9jZXNzIC1uZSAkcmFscGhPd25lciAtb3IgJHJhbHBoTGlzdGVuZXIuTG9jYWxBZGRyZXNzIC1ub3RpbiBAKCcxMjcuMC4wLjEnLCc6OjEnKSkgeyB0aHJvdyAnTGlzdGVuZXIgY2hhbmdlZCBkdXJpbmcgaGVhbHRoIG9ic2VydmF0aW9uJyB9CiAgfQogIEB7IGhvc3Q9W0Vudmlyb25tZW50XTo6TWFjaGluZU5hbWU7IHBpZD0kcmFscGhPd25lcjsgcG9ydD0kcmFscGhQb3J0OyBleGVjdXRhYmxlUGF0aD0kcmFscGhQcm9jZXNzLkV4ZWN1dGFibGVQYXRoOwogICAgIGltYWdlPSRyYWxwaFByb2Nlc3MuTmFtZTsgcHJvY2Vzc0NyZWF0ZWRVdGM9JHJhbHBoUHJvY2Vzcy5DcmVhdGlvbkRhdGUuVG9Vbml2ZXJzYWxUaW1lKCkuVG9TdHJpbmcoJ28nKTsKICAgICBsaXN0ZW5lckFkZHJlc3Nlcz1AKCRyYWxwaExpc3RlbmVyc0FmdGVyIHwgRm9yRWFjaC1PYmplY3QgeyAkXy5Mb2NhbEFkZHJlc3MgfSk7CiAgICAgc3RhYmxlPSR0cnVlOyBoZWFsdGg9JHJhbHBoSGVhbHRoIH0gfCBDb252ZXJ0VG8tSnNvbiAtRGVwdGggMTIgLUNvbXByZXNzCiAgZXhpdCAwCn0gY2F0Y2ggewogIFtDb25zb2xlXTo6RXJyb3IuV3JpdGVMaW5lKCRfLkV4Y2VwdGlvbi5NZXNzYWdlKQogIGV4aXQgMQp9Cg==\", \"timeoutSeconds\": 30, \"reason\": \"Read the discovered Fusion bridge process and its loopback health version without changing runtime state\"}", + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "caller": "Fusion mechanical Ralph", + "startedAt": 1791235896.927468, + "mutating": false, + "stdout": "{\n \"durationMs\": 7487,\n \"exitCode\": 0,\n \"ok\": true,\n \"schemaVersion\": 1,\n \"status\": \"ok\",\n \"stderr\": \"\",\n \"stdout\": \"{\\\"image\\\":\\\"adom-fusion-bridge.exe\\\",\\\"listenerAddresses\\\":[\\\"127.0.0.1\\\"],\\\"executablePath\\\":\\\"C:\\\\\\\\Users\\\\\\\\drew\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Adom Bridge\\\\\\\\bridges-cache\\\\\\\\fusion360\\\\\\\\adom-fusion-bridge.exe\\\",\\\"stable\\\":true,\\\"processCreatedUtc\\\":\\\"2026-10-05T21:01:31.6721740Z\\\",\\\"host\\\":\\\"ADOM_DREW2\\\",\\\"health\\\":{\\\"status\\\":\\\"ok\\\",\\\"busy\\\":false,\\\"longOp\\\":null,\\\"led\\\":\\\"green\\\",\\\"summary\\\":\\\"Fusion ready\\\",\\\"tooltip\\\":\\\"Fusion 360 running, AdomBridge add-in connected. All fusion_* verbs available.\\\",\\\"bridgeVersion\\\":\\\"1.12.0-dev.8\\\",\\\"fusion\\\":{\\\"installed\\\":true,\\\"exe_path\\\":\\\"C:\\\\\\\\Users\\\\\\\\drew\\\\\\\\AppData\\\\\\\\Local\\\\\\\\Autodesk\\\\\\\\webdeploy\\\\\\\\production\\\\\\\\6a0c9611291d45bb9226980209917c3d\\\\\\\\FusionLauncher.exe\\\",\\\"addins_dir\\\":\\\"C:\\\\\\\\Users\\\\\\\\drew\\\\\\\\AppData\\\\\\\\Roaming\\\\\\\\Autodesk\\\\\\\\FusionAddins\\\",\\\"addin_installed\\\":true,\\\"running\\\":true},\\\"addin\\\":{\\\"busy\\\":false,\\\"version\\\":\\\"1.12.0-dev.8\\\",\\\"pyThreads\\\":4},\\\"shell\\\":\\\"rust\\\",\\\"needsSignin\\\":false,\\\"licensingDialog\\\":false,\\\"windowTitle\\\":\\\"Untitled - Autodesk Fusion \\\",\\\"windowFactsDegraded\\\":false,\\\"windowFactsError\\\":null,\\\"exePort\\\":50618},\\\"pid\\\":42780,\\\"port\\\":50618}\\r\\n\",\n \"success\": true,\n \"timedOut\": false,\n \"timeoutSeconds\": 45\n}\n", + "stderr": "", + "returncode": 0, + "finishedAt": 1791235904.4366674, + "raw": { + "durationMs": 7487, + "exitCode": 0, + "ok": true, + "schemaVersion": 1, + "status": "ok", + "stderr": "", + "stdout": "{\"image\":\"adom-fusion-bridge.exe\",\"listenerAddresses\":[\"127.0.0.1\"],\"executablePath\":\"C:\\\\Users\\\\drew\\\\AppData\\\\Local\\\\Adom Bridge\\\\bridges-cache\\\\fusion360\\\\adom-fusion-bridge.exe\",\"stable\":true,\"processCreatedUtc\":\"2026-10-05T21:01:31.6721740Z\",\"host\":\"ADOM_DREW2\",\"health\":{\"status\":\"ok\",\"busy\":false,\"longOp\":null,\"led\":\"green\",\"summary\":\"Fusion ready\",\"tooltip\":\"Fusion 360 running, AdomBridge add-in connected. All fusion_* verbs available.\",\"bridgeVersion\":\"1.12.0-dev.8\",\"fusion\":{\"installed\":true,\"exe_path\":\"C:\\\\Users\\\\drew\\\\AppData\\\\Local\\\\Autodesk\\\\webdeploy\\\\production\\\\6a0c9611291d45bb9226980209917c3d\\\\FusionLauncher.exe\",\"addins_dir\":\"C:\\\\Users\\\\drew\\\\AppData\\\\Roaming\\\\Autodesk\\\\FusionAddins\",\"addin_installed\":true,\"running\":true},\"addin\":{\"busy\":false,\"version\":\"1.12.0-dev.8\",\"pyThreads\":4},\"shell\":\"rust\",\"needsSignin\":false,\"licensingDialog\":false,\"windowTitle\":\"Untitled - Autodesk Fusion \",\"windowFactsDegraded\":false,\"windowFactsError\":null,\"exePort\":50618},\"pid\":42780,\"port\":50618}\r\n", + "success": true, + "timedOut": false, + "timeoutSeconds": 45 + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/diagnostic_scene.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/diagnostic_scene.py new file mode 100644 index 0000000..2b6fe62 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/diagnostic_scene.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""CP9 owned scene diagnostic, default plan; no rendering or images in any mode.""" +import argparse +import hashlib +import importlib +import importlib.util +import json +from pathlib import Path +import signal +import sys + +PROBE_FILES = { + 'read_scene_probe.py':'bc2bcf78675f28151faa4ab0c67276e553792cc0c6c206fad98718219e02814c', + 'test_read_scene_probe.py':'8df35f06355717a0c8bfd64b66a951a4a349b9257aff0b14ff1db7a02edb8ba4', + 'READONLY-PROBE.md':'01f33dde1a9bfa821b308a91d0889cadce7c6098af73cdcfbe98e66bc527ff41'} +HARNESS_FILES = { + 'ralph_jobs.py':'4322d735104fb82f722fe03a1a806f6cc64fb264541ef22968111df14150a56f', + 'ralph_mechanical.py':'ab7451dbc3dda32442128a6262aebf3b9b53884571c6174e9bc2151e5f912d2e', + 'ralph_support.py':'a152fed6766ba0e86f89a84b52ce9090bcabcbc3bf888ae1b7f7b08836895e27', + 'fusion_harness.py':'0ded009f2ad0b5d8919ac215b9366df2b44d9ef98416e5f4a076e36656f88ec2'} + + +def need(condition,message): + if not condition: raise ValueError(message) + + +def sha(data): return hashlib.sha256(data).hexdigest() + + +def verified_sources(harness_evidence,probe_dir): + evidence=Path(harness_evidence).resolve();probe_dir=Path(probe_dir).resolve() + report=json.loads((evidence/'report.json').read_text()) + need(report.get('native')=='PASS' and report.get('cleanup')=='PASS' + and report.get('acceptanceScope',{}).get('ordinary')=='PASS','Accepted CP8 ordinary harness evidence is required') + rows=report.get('harnessSources',[]) + need(len(rows)==len(HARNESS_FILES) and {row['name'] for row in rows}==set(HARNESS_FILES),'CP8 harness source inventory differs') + files={};folders=set() + for row in rows: + name=row['name'];path=Path(row['path']).resolve() + need(path.is_relative_to(evidence) and path.name==name and row['sha256']==HARNESS_FILES[name] + and sha(path.read_bytes())==HARNESS_FILES[name],'Accepted CP8 harness bytes differ: '+name) + files[name]=path;folders.add(path.parent) + need(len(folders)==1,'CP8 harness source folders differ') + for name,expected in PROBE_FILES.items(): + path=probe_dir/name + need(sha(path.read_bytes())==expected,'Reviewed read-only probe bytes differ: '+name) + files['probe/'+name]=path + return folders.pop(),files + + +def load_sources(folder,files): + names=('fusion_harness','ralph_support','ralph_mechanical') + need(all(name not in sys.modules for name in names),'Refusing preloaded harness modules') + sys.path.insert(0,str(folder)) + modules={name:importlib.import_module(name) for name in names} + for name,module in modules.items(): + need(Path(module.__file__).resolve()==files[name+'.py'],'Harness import path differs') + spec=importlib.util.spec_from_file_location('cp9_reviewed_read_scene_probe',files['probe/read_scene_probe.py']) + probe=importlib.util.module_from_spec(spec);spec.loader.exec_module(probe) + return modules,probe + + +def validate_probe_result(value,session_id,run_id,document_id): + need(len(json.dumps(value,allow_nan=False).encode())<=128*1024,'Whole diagnostic output exceeds reviewed bound') + need(value.get('success') is True and value.get('sessionId')==session_id and value.get('ownerRunId')==run_id + and value.get('documentId')==document_id and value.get('acceptance')=='DIAGNOSTIC_ONLY','Diagnostic ownership/acceptance differs') + data=value.get('renderProbe',{}) + need(data.get('probeVersion')==1 and type(data.get('probeVersion')) is int and data.get('status')=='DIAGNOSTIC_ONLY' + and data.get('renderStarted') is False and data.get('settingsWritten') is False + and data.get('sceneWitnessComplete') is False and data.get('nativeQueueIdle')=='UNVERIFIED' + and data.get('exclusiveSettingsOwnership')=='UNVERIFIED','Diagnostic scope claims exceed the reviewed read-only probe') + need(data.get('documentFlagsUnchanged') is True,'Read-only probe did not establish unchanged document flags') + need(type(data.get('observationComplete')) is bool and isinstance(data.get('errors'),list) + and isinstance(data.get('gaps'),list) and data['gaps'],'Diagnostic completeness/gaps are absent') + need(all(isinstance(row,dict) and isinstance(row.get('field'),str) and isinstance(row.get('error'),str) + for row in data['errors']),'Per-field diagnostic errors are malformed') + need(not data['errors'] or data['observationComplete'] is False,'Diagnostic errors contradict observation completeness') + return {'qualification':'OBSERVED' if data['observationComplete'] else 'PARTIAL', + 'sceneWitnessComplete':False,'renderingQualified':False,'nativeQueueIdle':'UNVERIFIED', + 'exclusiveSettingsOwnership':'UNVERIFIED','fieldErrors':data['errors'],'gaps':data['gaps']} + + +def campaign_type(modules,probe,files): + base=modules['ralph_mechanical'];support=modules['ralph_support'] + class SceneDiagnostic(base.Campaign): + def __init__(self,args,client=None): + super().__init__(args,client) + snapshots=self.directory/('diagnostic-sources-'+self.id) + snapshots.mkdir(parents=True,exist_ok=False) + self.report['diagnosticSources']=[] + for name,path in {**files,'diagnostic_scene.py':Path(__file__)}.items(): + data=Path(path).read_bytes();destination=snapshots/name + destination.parent.mkdir(parents=True,exist_ok=True);destination.write_bytes(data) + self.report['diagnosticSources'].append({'name':name,'path':str(destination),'sha256':sha(data)}) + self.report['acceptanceScope']={'sceneDiagnostic':'REQUESTED','rendering':'NOT_RUN','futureLifecycle':'NOT_RUN', + 'sceneCompleteness':'NOT_PROVEN','queueOwnership':'NOT_PROVEN','visual':'NOT_REQUESTED'} + self.report['limitations']=['Read-only capability/scene field diagnostic only; partial availability is retained.', + 'No render future, scene/settings edit, global native queue or complete-scene proof.', + 'No capture, screenshot, image artifact or Shotlog path is enabled.'] + self.report['probeAttempted']=False + + def native(self,action,**values): + if action=='capture':raise RuntimeError('Images are disabled for this diagnostic') + return super().native(action,**values) + + def checkpoint(self,capture=False): + if capture is not False:raise RuntimeError('Diagnostic checkpoints require capture=False') + return super().checkpoint(capture=False) + + def save_artifact(self,value,kind): + if kind!='native-archive':raise RuntimeError('Only native archive artifacts are permitted') + return super().save_artifact(value,kind) + + def scene_diagnostic(self): + need(not self.report['probeAttempted'],'The scene probe is never replayed') + # Register the exact typed creation before optional assertions. + self.document_id=None + data=self.mechanical('new_design',write=True,name='Untitled',box={'width':20,'height':30,'depth':10}) + self.document_id=data['target']['documentId'];self.target=data['target'] + self.owned.manifest.setdefault('pendingRegistrations',{})[self.document_id]=data;self.owned.persist() + row=self.native('register',documentId=self.document_id,role=self.fixture) + self.bind_document(row) + del self.owned.manifest['pendingRegistrations'][self.document_id];self.owned.persist() + self.inspect() + self.measured('Diagnostic box',6000) + before=self.witness() + self.report['modelBeforeProbe']=before;self.persist() + program=probe.owned_script(support.native_script,run_id=self.id,host=self.args.host_name, + session_id=self.session,document_id=self.document_id) + (self.directory/'native-scene-probe.py').write_text(program) + self.report['probeAttempted']=True;self.persist() + # Read-only native work still has an uncertain lifecycle on timeout; + # inherited Campaign.call then retains ownership without cleanup. + response=self.call('fusion_run_modeling_script',{'script':program},mutating=True) + value=response['data']['result'] + self.report['sceneProbe']=value;self.persist() + after=self.witness();self.report['modelAfterProbe']=after;self.persist() + self.check('Independent fixture model witness unchanged across probe',support.equivalent(before,after)) + self.report['diagnosticQualification']=validate_probe_result(value,self.session,self.id,self.document_id) + self.check('Reviewed probe remains an owned read-only diagnostic',True,self.report['diagnosticQualification']) + self.report['acceptanceScope']['sceneDiagnostic']='PASS' + + def run(self): + code=super().run() + self.report['visual']={'status':'NOT_REQUESTED','reason':'This diagnostic forbids images; no visual acceptance claim.'} + if self.report.get('native')=='PASS' and self.report.get('cleanup')=='PASS': + self.report['overall']='PASS_DIAGNOSTIC' + else: + self.report['overall']='FAIL' + self.persist() + return code + return SceneDiagnostic + + +def main(argv=None): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('mode',choices=('plan','run'),nargs='?',default='plan') + for name in ('harness-evidence','target','host-name','evidence','expect-bridge','expect-addin'): + parser.add_argument('--'+name) + parser.add_argument('--probe-dir',default=str(Path(__file__).parent/'readonly-r2')) + args=parser.parse_args(argv) + if args.mode=='plan': + print(json.dumps({'hostCalls':False,'freshFixtures':1,'probeCalls':1,'images':False,'rendering':False, + 'checks':['exact typed fixture registration','independent before/after model witness','owned read-only diagnostic fields', + 'F3D archive/reopen witness','exact owned cleanup and original restoration'], + 'partialAvailability':'Retained explicitly; never complete-scene or native-queue qualification.'},indent=2)) + return 0 + need(all((args.harness_evidence,args.target,args.host_name,args.evidence,args.expect_bridge,args.expect_addin)), + 'Explicit accepted harness evidence, target, host, new evidence directory and expected versions required') + folder,files=verified_sources(args.harness_evidence,args.probe_dir) + modules,probe=load_sources(folder,files) + directory=Path(args.evidence);directory.mkdir(parents=True,exist_ok=False) + args.fixtures=['scene_diagnostic'];args.iterations=1;args.inject_failure=None;args.shotlog_channel=None + runner=campaign_type(modules,probe,files)(args) + def interrupt(signum,_):raise KeyboardInterrupt('Signal '+str(signum)) + signal.signal(signal.SIGTERM,interrupt);signal.signal(signal.SIGINT,interrupt) + code=runner.run() + print(json.dumps({'overall':runner.report['overall'],'report':str(directory/'report.json'),'images':False})) + return code + + +if __name__=='__main__':sys.exit(main()) diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/fusion_harness.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/fusion_harness.py new file mode 100644 index 0000000..8b898af --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/fusion_harness.py @@ -0,0 +1,472 @@ +"""Evidence, single-dispatch transport and owned-resource lifecycle for live tests. + +This module never starts Fusion, chooses a document by display name, or retries a +mutation. Campaigns supply their own exact-identity native cleanup adapters. +""" +from dataclasses import dataclass, field +import datetime +import hashlib +import json +from pathlib import Path, PureWindowsPath +import re +import subprocess +import time +import uuid + +ENVELOPES = ("output", "data", "result", "response", "structuredContent", "content") +# Transport metadata does not make an output summary into a native result. A +# native reply has its own data/result/content, or explicit success plus domain +# fields (for example bridges or addinResponding). Keep those replies intact. +TRANSPORT_FIELDS = set(ENVELOPES) | { + "success", "ok", "status", "isError", "error", "errorCode", "cause", "message", + "durationMs", "timeoutSeconds", "schemaVersion", "requestId", "command", "verb", + "target", "caller", "runId", "_hint", "_timeoutHint", "_skillsHint", +} + + +def native_payload(value): + return (any(key in value for key in ("data", "result", "content")) + or isinstance(value.get("success"), bool) and bool(set(value) - TRANSPORT_FIELDS)) + + +def transport_wrapper(value): + return any(key in value for key in ("output", "response", "structuredContent")) and not native_payload(value) +INFRASTRUCTURE = {"bridge_unavailable", "target_not_found", "bridge_restarting", + "main_thread_busy", "fusion_addin_busy", "addin_command_timeout", + "addin_main_thread_blocked", "fusion_not_running", "connection_error", "timeout", "runner_error"} + + +def infrastructure_failure(failure): + return failure.get("code") in INFRASTRUCTURE or any(word in str(failure.get("error", "")).lower() for word in + ("bridge disconnected", "connection refused", "connection reset", "bridge unavailable", "target_not_found", + "addin_command_timeout", "addin_main_thread_blocked", "main_thread_busy", "bridge_restarting", "timed out")) + + +def unresolved_operation(observation): + """Lifecycle evidence is not a terminal refusal, even with success:false.""" + return any(value.get("outcome") in ("pending", "unknown") + or value.get("reconciliationRequired") is True + or value.get("errorCode") in ("operation_in_progress", "operation_reconciliation_required") + for _, value in observation.envelopes) + + +def mechanical_request_digest(request): + """Independent digest of the public user payload, excluding relay metadata.""" + payload = {key:value for key,value in request.items() if key not in ("caller", "_callerBudgetMs")} + return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")).hexdigest() + + +def run_id(): + return datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + uuid.uuid4().hex + + +@dataclass +class Observation: + payload: dict + failures: list = field(default_factory=list) + positive: bool = False + envelopes: list = field(default_factory=list) + + +def observe(raw): + """Preserve the semantic envelope; errors anywhere outrank outer success. + + Only explicit status markers are errors (isValid:false and ordinary domain + booleans are data). JSON in documented envelope/text slots is decoded without + overwriting an enclosing status, error or code. + """ + failures, envelopes = [], [] + positive = False + visited = 0 + + def walk(value, path="$", depth=0, envelope=True): + nonlocal positive, visited + # Domain objects (catalog schemas, geometry, file metadata) are not + # response envelopes. Their keys may themselves document error fields; + # traversing them adds no semantic check and can exhaust the budget on + # a legitimate duplicated describe response before reaching its status. + if not envelope: + return + visited += 1 + if visited > 20000 or depth > 40: + raise ValueError("response inspection budget exceeded") + if isinstance(value, str) and envelope and value.lstrip().startswith(("{", "[")): + value = json.loads(value) + if isinstance(value, dict): + if envelope: + envelopes.append((path, value)) + error = value.get("error") + failed = value.get("success") is False or value.get("ok") is False or value.get("isError") is True + status = value.get("status") + failed = failed or isinstance(status, str) and status.lower() in {"error", "failed", "failure", "cancelled", "canceled", "timeout"} + failed = failed or error not in (None, "", False, {}, []) or bool(value.get("errorCode")) + if envelope and failed: + failures.append({"path": path, "error": error, "code": value.get("errorCode"), + "cause": value.get("cause"), "status": status}) + positive = positive or envelope and not transport_wrapper(value) and (value.get("success") is True or value.get("ok") is True or status == "ok") + for key, child in value.items(): + if isinstance(child, (dict, list)) or key in ENVELOPES or key == "text": + script_stdout = key == "output" and isinstance(child, str) and isinstance(value.get("success"), bool) and isinstance(value.get("data"), dict) and "result" in value["data"] + summary_text = key == "output" and isinstance(child, str) and native_payload(value) and not child.lstrip().startswith(("{", "[")) + if envelope and key == "output" and transport_wrapper(value): + decoded = json.loads(child) if isinstance(child, str) else child + if not isinstance(decoded, dict): raise ValueError("Transport output must contain a JSON object") + walk(child, path + "." + key, depth + 1, not (script_stdout or summary_text) and (envelope and key in ENVELOPES or key == "text" and ".content[" in path)) + elif isinstance(value, list): + for index, child in enumerate(value): + walk(child, path + "[%d]" % index, depth + 1, envelope) + + try: + walk(raw) + except (ValueError, TypeError, RecursionError) as exc: + failures.append({"path": "$", "code": "malformed_response", "error": str(exc)}) + payload = raw if isinstance(raw, dict) else {} + # Output/response are transport wrappers; data and native result stay nested so callers + # retain native success/error beside it. Never merge dictionaries together. + for _ in range(40): + if not isinstance(payload, dict): break + if native_payload(payload): break + child = next((payload[k] for k in ("output", "response", "structuredContent") + if isinstance(payload.get(k), dict) or isinstance(payload.get(k), str) and payload[k].lstrip().startswith("{")), None) + if isinstance(child, str): + try: child = json.loads(child) + except ValueError: break + if not isinstance(child, dict): + break + payload = child + if not isinstance(raw, dict): + failures.append({"path": "$", "code": "malformed_response", "error": "Expected an object response."}) + return Observation(payload, failures, positive, envelopes) + + +def data_payload(observation): + value = observation.payload + while isinstance(value.get("data"), dict): + value = value["data"] + return value + + +def evaluate(raw, spec): + observation = observe(raw) + data = data_payload(observation) + errors = [] + if spec.get("pred") not in (None, "count_gt_0", "must_refuse", "verbs_match_plan", "ok_or_budget"): + return ["unknown expectation predicate: " + str(spec["pred"])] + failures = observation.failures + infrastructure = [f for f in failures if infrastructure_failure(f)] + if infrastructure or isinstance(raw, dict) and raw.get("_testTimeout"): + return ["infrastructure failure; this does not prove an expected refusal"] + expected = spec.get("errorCode") + refusal = spec.get("refusal", {}) + expected = refusal.get("code", expected) + if spec.get("pred") == "must_refuse" and not expected: + return ["negative case lacks an explicit refusal code/cause contract"] + alternative = spec.get("okOrCode") + if expected or alternative and failures: + code = expected or alternative + matching = [f for f in failures if f.get("code") == code] + if not matching: + errors.append("expected refusal code %s; observed %s" % (code, [f.get("code") for f in failures])) + if refusal.get("cause") is not None and not any(f.get("cause") == refusal["cause"] for f in matching): + errors.append("expected refusal cause %s" % refusal["cause"]) + # Wrapping success:false without a code is normal. A competing coded + # failure means the intended guard was not the sole observed outcome. + if any(f.get("code") not in (None, code) for f in failures): + errors.append("unexpected additional failure code") + else: + if failures: + errors.append("semantic failure: " + json.dumps(failures, default=str)) + if not observation.positive: + errors.append("no explicit successful response") + for name in spec.get("fields", []): + if name not in data and name not in observation.payload: + errors.append("missing field %r" % name) + if spec.get("pred") == "count_gt_0" and not (isinstance(data.get("count"), (int, float)) and data["count"] > 0 or isinstance(data.get("results"), list) and data["results"]): + errors.append("expected >0 results") + if spec.get("pred") == "ok_or_budget": + errors.append("BOM success and budget refusal need separate explicit fixture cases") + if spec.get("pred") == "verbs_match_plan" and not (isinstance(data.get("verbs"), list) and data["verbs"]): + errors.append("runtime catalog must be a nonempty list") + return errors + + +@dataclass +class CallResult: + raw: dict + observation: Observation + attempts: list + timed_out: bool + elapsed: float + mutating: bool = False + + @property + def ambiguous(self): + first = self.attempts[0] if self.attempts else {} + try: request = json.loads(first.get("requestJson", "{}")) + except (ValueError, TypeError): request = {} + excluded = set() + for path, value in self.observation.envelopes: + durability = value.get("durability") + if (first.get("verb") == "fusion_mechanical" and isinstance(request, dict) + and isinstance(durability, dict) and durability.get("terminalRecorded") is True + and durability.get("phase") == "not_executed" + and value.get("success") is False and value.get("nativeExecutionStarted") is False + and request.get("sessionId") and request.get("operationId") + and value.get("sessionId") == request["sessionId"] + and value.get("operationId") == request["operationId"] + and durability.get("recordedSessionId") == request["sessionId"]): + # A persisted skip proves no native execution for this exact + # request. Its timeout wording is not an unknown outcome. + excluded.add(path) + uncertain = any((infrastructure_failure(f) and f["path"] not in excluded) + or f.get("code") == "malformed_response" for f in self.observation.failures) + absent = not self.observation.positive and not self.observation.failures + return self.mutating and (self.timed_out or uncertain or absent or unresolved_operation(self.observation)) + + +class BridgeClient: + def __init__(self, target, run_id, ai_thread="Fusion harness", evidence_dir=None, runner=subprocess.run): + self.target, self.run_id, self.ai_thread = target, run_id, ai_thread + self.evidence_dir = Path(evidence_dir) if evidence_dir else None + if self.evidence_dir: self.evidence_dir.mkdir(parents=True, exist_ok=True) + self.runner, self.calls = runner, [] + + def call(self, verb, args=None, timeout=90, mutating=False): + started = time.time() + request_json = json.dumps(args or {}, allow_nan=False) + attempt = {"target": self.target, "verb": verb, "args": json.loads(request_json), "requestJson": request_json, "runId": self.run_id, + "caller": self.ai_thread, "startedAt": started, "mutating": mutating} + timed_out = False + try: + process = self.runner(["adom-bridge", "--target", self.target, "--ai-thread", self.ai_thread, + verb, request_json], capture_output=True, text=True, timeout=timeout) + attempt.update(stdout=process.stdout, stderr=process.stderr, returncode=process.returncode) + output = process.stdout.strip() + try: raw = json.loads(output) + except ValueError: + raw = {"success": False, "errorCode": "runner_error", "error": "CLI did not return a JSON response"} + if not isinstance(raw, dict): + raw = {"success": False, "errorCode": "runner_error", "error": "CLI returned a non-object response", "response": raw} + if process.returncode: + raw = {"success": False, "errorCode": "runner_error", "error": "CLI exited with code %s" % process.returncode, "output": raw} + except subprocess.TimeoutExpired as exc: + timed_out = True + attempt.update(stdout=exc.stdout.decode(errors="replace") if isinstance(exc.stdout, bytes) else exc.stdout, + stderr=exc.stderr.decode(errors="replace") if isinstance(exc.stderr, bytes) else exc.stderr) + raw = {"success": False, "errorCode": "timeout", "error": "CLI timeout; operation outcome is unknown", "_testTimeout": True} + except Exception as exc: + raw = {"success": False, "errorCode": "runner_error", "error": str(exc)} + attempt.update(finishedAt=time.time(), raw=json.loads(json.dumps(raw))) + result = CallResult(raw, observe(raw), [attempt], timed_out, time.time()-started, mutating) + self.calls.append(result) + if self.evidence_dir: + path = self.evidence_dir / ("%04d-%s.json" % (len(self.calls), re.sub(r"[^A-Za-z0-9_-]", "_", verb))) + path.write_text(json.dumps(attempt, indent=2, default=str)) + return result + + def reconcile_mechanical(self, original, session_id, operation_id, polls=3, interval=1): + """Status-only reconciliation; an unknown/running entry never means retry.""" + evidence = [] + first = original.attempts[0] if original.attempts else {} + try: args = json.loads(first.get("requestJson", "{}")) + except ValueError: args = {} + if not isinstance(args, dict) or first.get("verb") != "fusion_mechanical" or args.get("sessionId") != session_id or args.get("operationId") != operation_id or not original.mutating: + return {"state": "ambiguous", "original": original.attempts, "reconciliation": [], + "reason": "Reconciliation IDs must match the original typed mutation."} + for index in range(polls): + result = self.call("fusion_mech_operation_status", {"sessionId": session_id, "operationId": operation_id}) + evidence.append(result.attempts[0]) + data = data_payload(result.observation) + unrelated_failure = any(".result" not in f["path"] for f in result.observation.failures) + if not unrelated_failure and data.get("success") is True and data.get("sessionId") == session_id and data.get("operationId") == operation_id and data.get("state") in ("succeeded", "failed") and isinstance(data.get("result"), dict): + stored = observe(data["result"]) + terminal = data["result"] + durability = terminal.get("durability", {}) + identity_matches = (terminal.get("sessionId") == session_id + and terminal.get("operationId") == operation_id + and isinstance(durability, dict) + and durability.get("recordedSessionId", session_id) == session_id) + history = data.get("history", {}) + digest_matches = (isinstance(history, dict) and + ("requestDigest" not in history or history["requestDigest"] == mechanical_request_digest(args))) + phases = ("succeeded",) if data["state"] == "succeeded" else ("failed", "not_executed") + durability_matches = (isinstance(durability, dict) + and ("terminalRecorded" not in durability or durability["terminalRecorded"] is True) + and ("phase" not in data or data["phase"] in phases) + and ("phase" not in durability or durability["phase"] in phases) + and ("phase" not in data or "phase" not in durability or data["phase"] == durability["phase"])) + phase = data.get("phase", durability.get("phase") if isinstance(durability, dict) else None) + execution = (data.get("nativeExecutionStarted"), terminal.get("nativeExecutionStarted")) + execution_matches = all(value is None or isinstance(value, bool) for value in execution) + if phase == "not_executed": + execution_matches = execution_matches and terminal.get("nativeExecutionStarted") is False and data.get("nativeExecutionStarted") is not True + elif phase in ("succeeded", "failed"): + execution_matches = execution_matches and all(value is not False for value in execution) + coherent = identity_matches and digest_matches and durability_matches and execution_matches and not unresolved_operation(result.observation) and ( + data["state"] == "succeeded" and data["result"].get("success") is True and not stored.failures + or data["state"] == "failed" and data["result"].get("success") is False and bool(stored.failures)) + if coherent: + return {"state": data["state"], "result": data["result"], "original": original.attempts, "reconciliation": evidence} + if index + 1 < polls: time.sleep(interval) + return {"state": "ambiguous", "original": original.attempts, "reconciliation": evidence, + "reason": "No terminal ledger result; mutation was not replayed."} + + +class OwnedRun: + """Persist ownership before effects; cleanup always requires exact adapters. + + Document IDs are live session identities, never names or creationId alone. + A native checkpoint must be verified before the document closer is invoked. + verify_owner returns the exact documentId and ownerRunId observed in Fusion. + Cleanup callbacks return {success:true}; errors stay in the manifest/report. + """ + def __init__(self, manifest_path, target, run_id=None, host_scratch=None): + self.path = Path(manifest_path) + if self.path.exists(): raise ValueError("Ownership manifest already exists; never overwrite a prior run") + self.run_id = run_id or globals()["run_id"]() + if not re.fullmatch(r"[A-Za-z0-9_-]{12,100}", self.run_id): raise ValueError("Invalid run ID") + self.manifest = {"runId": self.run_id, "target": target, "state": "running", "documents": {}, "files": {}, "preferences": {}, "customCleanup": [], "cleanup": []} + self.callbacks = [] + self.documents = {} + self.file_adapters = {} + self.preference_adapters = {} + self.set_host_scratch(host_scratch or "C:/tmp/fusion-verb-tests/" + self.run_id) + + def persist(self): + self.path.parent.mkdir(parents=True, exist_ok=True) + temporary = self.path.with_suffix(self.path.suffix + ".tmp") + temporary.write_text(json.dumps(self.manifest, indent=2, default=str)) + temporary.replace(self.path) + + def set_host_scratch(self, path): + root = PureWindowsPath(path) + if not root.is_absolute() or ".." in root.parts or root.name != self.run_id: raise ValueError("Scratch root must be an absolute directory ending in the unique run ID") + if self.manifest["files"]: raise ValueError("Scratch root cannot change after file registration") + self.manifest["hostScratch"] = root.as_posix() + self.persist() + + @classmethod + def resume(cls, manifest_path, target, run_id=None): + manifest = json.loads(Path(manifest_path).read_text()) + rid = manifest.get("runId", "");root = PureWindowsPath(manifest.get("hostScratch", "")) + if manifest.get("target") != target or not re.fullmatch(r"[A-Za-z0-9_-]{12,100}", rid) or run_id is not None and run_id != rid: raise ValueError("Manifest target/run identity mismatch") + if not root.is_absolute() or ".." in root.parts or root.name != rid: raise ValueError("Invalid saved scratch ownership") + for name in manifest.get("files", {}): + path = PureWindowsPath(name) + if ".." in path.parts or root not in path.parents: raise ValueError("Saved file escapes owned scratch") + result = cls.__new__(cls) + result.path, result.run_id, result.manifest = Path(manifest_path), rid, manifest + result.callbacks, result.documents, result.file_adapters, result.preference_adapters = [], {}, {}, {} + return result + + def register_document(self, document_id, role, metadata=None, cleanup=None, verify_owner=None): + if not isinstance(document_id, str) or not document_id or document_id in self.manifest["documents"]: raise ValueError("A unique exact runtime document ID is required") + self.manifest["documents"][document_id] = {"role": role, "metadata": metadata or {}, "checkpoint": None} + self.documents[document_id] = (cleanup, verify_owner) + self.persist() + + def bind_document_cleanup(self, document_id, cleanup, verify_owner): + if document_id not in self.manifest["documents"]: raise ValueError("Document is not registered") + self.documents[document_id] = (cleanup, verify_owner) + + def register_file(self, path, sha256=None, cleanup=None, verify_owner=None, retain=False): + file = PureWindowsPath(path);root = PureWindowsPath(self.manifest["hostScratch"]) + if not file.is_absolute() or ".." in file.parts or file == root or root not in file.parents: raise ValueError("File must be below this run's scratch root") + if sha256 is not None and not re.fullmatch(r"[0-9a-fA-F]{64}", sha256): raise ValueError("Invalid file hash") + self.manifest["files"][file.as_posix()] = {"sha256": sha256, "retain": retain} + self.file_adapters[file.as_posix()] = (cleanup, verify_owner) + self.persist() + + def mark_checkpoint(self, document_id, path, sha256, verified=False): + if document_id not in self.manifest["documents"]: raise ValueError("Document is not owned") + self.register_file(path, sha256, retain=True) + self.manifest["documents"][document_id]["checkpoint"] = {"path": path, "sha256": sha256, "verified": verified is True} + self.persist() + + def add_cleanup(self, label, callback): + if label not in self.manifest["customCleanup"]: self.manifest["customCleanup"].append(label) + self.callbacks = [(name, action) for name, action in self.callbacks if name != label] + [(label, callback)] + self.persist() + + def preserve_preference(self, name, read, write): + original = read() + if name in self.manifest["preferences"]: raise ValueError("Preference already tracked; rebind its adapter on resume") + self.manifest["preferences"][name] = {"original": original, "restoreRequired": False} + self.preference_adapters[name] = (read, write) + self.persist() + def set_value(value): + self.manifest["preferences"][name].update(requested=value, restoreRequired=True) + self.persist() + write(value) + return set_value + + def bind_preference_restore(self, name, read, write): + if name not in self.manifest["preferences"]: raise ValueError("Preference is not tracked") + self.preference_adapters[name] = (read, write) + + def cleanup(self): + entries = [] + for document_id, record in self.manifest["documents"].items(): + close, verify = self.documents.get(document_id, (None, None)) + def finish(document_id=document_id, record=record, close=close, verify=verify): + if not (record.get("checkpoint") or {}).get("verified"): + raise ValueError("No verified native checkpoint; owned document retained") + if not close or not verify: raise ValueError("Exact-identity cleanup adapter unavailable; document retained") + owner = verify(document_id) + if observe(owner).failures: raise ValueError("Document ownership verification failed; refusing close") + if owner.get("documentId") != document_id or owner.get("ownerRunId") != self.run_id: + raise ValueError("Document ownership changed; refusing close") + result = close(document_id) + data = data_payload(observe(result)) + if data.get("closedDocumentId") != document_id or data.get("closed") is not True: + raise ValueError("Closer did not verify the exact document was closed") + return result + entries.append(("close owned document " + document_id, finish)) + for path, record in self.manifest["files"].items(): + if record.get("retain"): continue + remove, verify = self.file_adapters.get(path, (None, None)) + def finish_file(path=path, record=record, remove=remove, verify=verify): + if not remove or not verify: raise ValueError("Owned file cleanup adapter unavailable; file retained") + owner = verify(path) + if observe(owner).failures: raise ValueError("File ownership verification failed; refusing deletion") + if owner.get("ownerRunId") != self.run_id or PureWindowsPath(owner.get("path", "")) != PureWindowsPath(path) or owner.get("isSymlink") is not False: + raise ValueError("File ownership changed; refusing deletion") + if record.get("sha256") and owner.get("sha256") != record["sha256"]: raise ValueError("Owned file changed; refusing deletion") + return remove(path) + entries.append(("remove owned file " + path, finish_file)) + for name, record in self.manifest["preferences"].items(): + if not record.get("restoreRequired"): continue + def restore(name=name, record=record): + if name not in self.preference_adapters: raise ValueError("Preference restoration adapter unavailable; recovery remains unresolved") + read, write = self.preference_adapters[name] + current = read() + if current != record["original"]: + if current != record.get("requested"): raise ValueError("Preference changed concurrently; refusing overwrite") + write(record["original"]) + restored = read() == record["original"] + if restored: + record["restoreRequired"] = False + self.persist() + return {"success": restored, "preference": name} + entries.append(("restore preference " + name, restore)) + custom = dict(self.callbacks) + for label in reversed(self.manifest["customCleanup"]): + entries.append((label, custom.get(label, lambda: {"success": False, "error": "Cleanup adapter unavailable after interruption"}))) + for label, callback in entries: + if any(row["label"] == label and row["status"] == "PASS" for row in self.manifest["cleanup"]): continue + try: + raw = callback() + errors = evaluate(raw, {"ok": True}) + row = {"label": label, "status": "FAIL" if errors else "PASS", "errors": errors, "raw": raw} + except BaseException as exc: + row = {"label": label, "status": "FAIL", "error": str(exc)} + self.manifest["cleanup"].append(row) + self.persist() + latest = {row["label"]: row for row in self.manifest["cleanup"]} + self.manifest["cleanupLatest"] = latest + self.manifest["state"] = "cleanup_failed" if any(row["status"] != "PASS" for row in latest.values()) else "complete" + self.persist() + return self.manifest["cleanup"] + + def __enter__(self): return self + def __exit__(self, *_): self.cleanup() diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/READONLY-PROBE.md b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/READONLY-PROBE.md new file mode 100644 index 0000000..892fe17 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/READONLY-PROBE.md @@ -0,0 +1,91 @@ +# Read-only runtime capability and scene diagnostic + +The frozen `read_scene_probe.py` is research support, not a render implementation. +No native invocation has run. It reads one already-active, exact-owned native +Design and emits at most 128 KiB of structured data. There are no renders, images, +feeds, Shotlog uploads, workspace activations, setting assignments, transactions, +cloud calls, row activations, saves, closes or fixture creation. State and logs are +the default evidence. Any later visual test requires a meaningful visual change +or failure and deduplication before publishing; this diagnostic captures no images. + +## Entry points + +`render_capability_probe(app, document, design, max_environments=16)` must run on +Fusion's main thread. The caller must already hold exact ownership of `document`. +It verifies active-document and Design-parent API identity and exactly one matching +open object before reading rendering APIs. The open inventory limit is 128; the +optional environment budget is an integer from 0 through 32. + +`owned_script(native_script_factory, run_id=..., host=..., session_id=..., +document_id=...)` generates a program using the maintained Ralph native registry. +Pass `ralph_mechanical.native_script` as the factory. This function only returns +source text; it never dispatches. The generated program reuses its exact +`owned(documentId)`/`require` guard definitions but omits the generic inventory and +action branches. A main-thread check precedes all native ownership reads. After +exact session/run checks and bounded open/baseline inventories, it requires the +owned document to be already active and stores the diagnostic at `result['renderProbe']`. The root +caller controls the public transport and preserves its full raw envelope. + +Do not call the legacy modeling-script transport when no document is open: that +transport can create a document before this program runs. Root must independently +confirm the existing owned active fixture and runtime session first. The program +does not establish baseline user-document restoration or authorize fixture cleanup. +It also does not create or bind an ownership registry entry. The complete result +contains only success, document/run/session identities, acceptance and renderProbe; +the 128 KiB cap applies to this whole result. Historical registry rows are neither +walked nor serialized. + +## Evidence and limits + +- Per-field rows distinguish available values, thrown/absent getters, and conditional + getters deliberately not read. Missing classes and members are explicit static + Python-surface observations; a dynamic wrapper may require later native qualification. +- Camera is a detached viewport snapshot and is never assigned back. Eye, target and + orthographic extents are reported in mm from native cm; up-vector components are + unitless. Perspective angle remains a named native angular scalar whose unit + convention is unqualified in this diagnostic. +- Render options report dimensions in pixels, enum values, quality and transparency. + Scene ground offset is converted from cm to mm; lighting rotation from radians to + degrees; camera focal length remains native mm. Brightness is lux and exposure EV. + Negative finite offsets are retained; invalid values and conversion overflow are + isolated as field errors. Solid color RGBA channels are native 0..255 values. +- DOF, flattened-ground, ground-reflection and background-dependent getters are read + only when their corresponding flag/mode is known. The selected environment and a + bounded list of environment IDs/names/custom flags are separate observations. +- `observationComplete` means the bounded observation had no missing fields/classes, + errors or truncated environment enumeration. `documentFlagsUnchanged` separately + reports observed saved/modified/active flags. Neither field proves a full scene + witness, source stability, queue idleness, render success or settings ownership. +- `sceneWitnessComplete` is always false. `nativeQueueIdle` and + `exclusiveSettingsOwnership` remain `UNVERIFIED`. The probe does not enumerate + material/appearance/texture/light/occurrence-visibility graphs, hash custom + environment content, instantiate/poll RenderFuture, or establish scene-to-pixels + timing. These gaps stay in the returned object even when every scalar read works. + +## Offline validation + +```sh +cd /home/adom/project/research/fusion-roadmap-build-2026-10-02/cp9-render-preparation +python3 -m unittest test_read_scene_probe -v +``` + +Fourteen tests pass: setter and conditional-getter traps; exact document/owner and +main-thread guards; missing API surfaces; scalar units and overflow; bounded huge +collections/strings; orthographic acknowledgement; and generated-source mutation +exclusion; the actual maintained wrapper with 1,001 historical rows; worker refusal +before ownership reads; and whole-result size refusal. This is not native rendering acceptance. The original implementation +contract and future mutating probe proposal remain unchanged and separately gated. + +## Primary API references + +[RenderManager](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_RenderManager.htm), +[Rendering](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_Rendering.htm), +[SceneSettings](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_SceneSettings.htm), +[RenderFuture](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_RenderFuture.htm), +and [RenderEnvironment](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_RenderEnvironment.htm) +define the inspected surfaces. Unit-specific references are +[groundOffset](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_SceneSettings_groundOffset.htm), +[lightAngle](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_SceneSettings_lightAngle.htm), +[cameraFocalLength](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/fusion_SceneSettings_cameraFocalLength.htm), +[Camera.getExtents](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/core_Camera_getExtents.htm), +and [Color.opacity](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/core_Color_opacity.htm). diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/read_scene_probe.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/read_scene_probe.py new file mode 100644 index 0000000..7117381 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/read_scene_probe.py @@ -0,0 +1,269 @@ +"""Bounded CP9 native read-only probe; import alone makes no native calls. + +Use owned_script with the maintained native registry. Availability is not render +success, native queue idleness, a complete scene witness or settings ownership. +""" +import inspect +import ast +import json +import math +from pathlib import Path +import threading + +MAX_ENVIRONMENTS = 32 +MAX_OUTPUT_BYTES = 128 * 1024 + + +def render_capability_probe(app, document, design, *, max_environments=16): + import adsk.core + import adsk.fusion + + def need(value, message): + if not value: raise RuntimeError(message) + + need(threading.current_thread() is threading.main_thread(), 'Native render reads require the main thread') + need(type(max_environments) is int and 0 <= max_environments <= MAX_ENVIRONMENTS, 'Invalid environment probe budget') + need(document is not None and document.isValid and design is not None and design.isValid, 'Owned native design is invalid') + need(app.activeDocument == document and design.parentDocument == document, 'Current native document/design identity differs') + document_count = app.documents.count + need(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget or has invalid count') + need(sum(app.documents.item(i) == document for i in range(document_count)) == 1, 'Exact owned document is not uniquely open') + result = {'probeVersion':1, 'status':'DIAGNOSTIC_ONLY', 'renderStarted':False, 'settingsWritten':False, + 'sceneWitnessComplete':False, 'nativeQueueIdle':'UNVERIFIED', 'exclusiveSettingsOwnership':'UNVERIFIED', + 'errors':[], 'classes':{}, 'camera':{}, 'rendering':{}, 'scene':{}, 'environments':{}, + 'gaps':['No render execution or RenderFuture instance was created.', + 'No global renderer queue/coexistence proof.', + 'Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.', + 'Environment IDs and names do not prove custom environment content identity.', + 'No admitted revision-to-pixels or full source snapshot claim.']} + + def text(value): + need(isinstance(value,str) and len(value) <= 256, 'Native string exceeds probe budget or has wrong type') + return value + + def number(value): + need(type(value) in (int,float) and math.isfinite(value), 'Native scalar is not a finite number') + return value + + def boolean(value): + need(type(value) is bool, 'Native scalar is not Boolean') + return value + + def enum(value): + need(not isinstance(value,(bool,str)), 'Native enum has invalid type') + result = int(value) + need(result == value, 'Native enum is not an exact integer') + return result + + def point(value, scale=10): + return [number(number(getattr(value,axis)) * scale) for axis in ('x','y','z')] + + def read(path, callback, unit=None, native_unit=None): + try: + data = callback() + row = {'available':True,'value':data} + if unit is not None: row['unit'] = unit + if native_unit is not None: row['nativeUnit'] = native_unit + return row + except Exception as error: + reason = str(error)[:512] + result['errors'].append({'field':path,'error':reason}) + return {'available':False,'error':reason} + + def skipped(reason): return {'available':None,'notRead':reason} + + def object_read(path, callback): + try: + value = callback() + need(value is not None and value.isValid, 'Native object absent or invalid') + text(value.objectType) + return value, {'available':True,'objectType':value.objectType} + except Exception as error: + reason = str(error)[:512] + result['errors'].append({'field':path,'error':reason}) + return None, {'available':False,'error':reason} + + def fields(destination, prefix, owner, specs): + for name, converter, unit, native_unit in specs: + destination[name] = read(prefix + '.' + name, lambda name=name,converter=converter:converter(getattr(owner,name)), unit, native_unit) + + result['documentBefore'] = {'modified':read('document.modified',lambda:boolean(document.isModified)), + 'saved':read('document.saved',lambda:boolean(document.isSaved))} + # Static class inspection does not invoke descriptors, constructors, start, + # activation, cancel or any native future polling. + class_members = {'RenderManager':('rendering','sceneSettings','renderEnvironments','lights','isRenderWorkspaceActive'), + 'Rendering':('startLocalRender','aspectRatio','resolution','resolutionWidth','resolutionHeight','renderQuality','isBackgroundTransparent'), + 'RenderFuture':('filename','imageWidth','imageHeight','progress','renderState','isValid'), + 'SceneSettings':('backgroundType','backgroundEnvironment','backgroundSolidColor','isDepthOfFieldEnabled'), + 'RenderEnvironment':('id','name','isCustomEnvironment')} + for name,members in class_members.items(): + try: + cls = getattr(adsk.fusion,name) + availability = {} + for member in members: + try: inspect.getattr_static(cls,member); availability[member] = True + except AttributeError: availability[member] = False + result['classes'][name] = {'available':True,'members':availability,'methodInvocation':False} + except Exception as error: + result['classes'][name] = {'available':False,'error':str(error)[:512]} + + camera_enums = {name:read('CameraTypes.'+name,lambda name=name:enum(getattr(adsk.core.CameraTypes,name))) + for name in ('OrthographicCameraType','PerspectiveCameraType','PerspectiveWithOrthoFacesCameraType')} + background_enums = {name:read('RenderSceneBackgroundTypes.'+name, + lambda name=name:enum(getattr(adsk.fusion.RenderSceneBackgroundTypes,name))) + for name in ('SolidColorRenderSceneBackgroundType','EnvironmentRenderSceneBackgroundType')} + result['enums'] = {'camera':camera_enums,'background':background_enums} + + viewport, result['viewport'] = object_read('activeViewport',lambda:app.activeViewport) + camera = None + if viewport is not None: + camera, result['camera']['object'] = object_read('camera',lambda:viewport.camera) + if camera is not None: + fields(result['camera'],'camera',camera,[('cameraType',enum,'native CameraTypes enum',None), + ('eye',point,'mm','cm'),('target',point,'mm','cm'),('upVector',lambda p:point(p,1),'unitless',None), + ('isFitView',boolean,None,None),('isSmoothTransition',boolean,None,None)]) + result['camera']['space'] = 'world/root' + kind = result['camera']['cameraType'].get('value') + if kind is not None and kind == camera_enums['OrthographicCameraType'].get('value'): + def extents(): + okay,width,height = camera.getExtents() + need(okay is True, 'Orthographic camera extent read refused') + return [number(number(width)*10),number(number(height)*10)] + result['camera']['extents'] = read('camera.getExtents',extents,'mm','cm') + result['camera']['perspectiveAngle'] = skipped('Orthographic camera') + elif kind is not None and kind in (camera_enums['PerspectiveCameraType'].get('value'),camera_enums['PerspectiveWithOrthoFacesCameraType'].get('value')): + result['camera']['perspectiveAngle'] = read('camera.perspectiveAngle',lambda:number(camera.perspectiveAngle), + 'native angular scalar; convention requires native qualification') + result['camera']['extents'] = skipped('Perspective camera') + else: + result['errors'].append({'field':'camera.cameraType','error':'Unknown/unreadable camera mode; conditional lens getters not read'}) + result['camera']['extents'] = result['camera']['perspectiveAngle'] = skipped('Unknown camera mode') + + manager, result['renderManager'] = object_read('design.renderManager',lambda:design.renderManager) + if manager is not None: + matched = read('renderManager.parentDesign',lambda:boolean(manager.parentDesign == design)) + result['renderManager']['ownerMatches'] = matched + if matched.get('value') is not True: + result['errors'].append({'field':'renderManager.parentDesign','error':'Foreign/unreadable renderer owner; remaining renderer objects not read'}) + manager = None + if manager is not None: + result['renderManager']['workspaceActive'] = read('renderManager.isRenderWorkspaceActive',lambda:boolean(manager.isRenderWorkspaceActive)) + rendering, result['rendering']['object'] = object_read('renderManager.rendering',lambda:manager.rendering) + if rendering is not None: + fields(result['rendering'],'rendering',rendering,[('aspectRatio',enum,'native RenderAspectRatios enum',None), + ('resolution',enum,'native RenderResolutions enum',None),('resolutionWidth',number,'px',None), + ('resolutionHeight',number,'px',None),('renderQuality',number,'native quality (25..100)',None), + ('isBackgroundTransparent',boolean,None,None)]) + scene, result['scene']['object'] = object_read('renderManager.sceneSettings',lambda:manager.sceneSettings) + if scene is not None: + specs = [('aspectRatio',enum,'native RenderAspectRatios enum',None),('aspectRatioWidth',number,'ratio',None), + ('aspectRatioHeight',number,'ratio',None),('backgroundType',enum,'native RenderSceneBackgroundTypes enum',None), + ('brightness',number,'lux',None),('cameraExposure',number,'EV',None),('cameraFocalLength',number,'mm','mm'), + ('cameraType',enum,'native CameraTypes enum',None),('groundOffset',lambda n:number(number(n)*10),'mm','cm'), + ('lightAngle',lambda n:number(math.degrees(number(n))),'deg','rad')] + specs += [(name,boolean,None,None) for name in ('isDepthOfFieldEnabled','isGroundDisplayed','isGroundFlattened','isGroundReflections')] + fields(result['scene'],'scene',scene,specs) + for condition,names in (('isDepthOfFieldEnabled',('centerOfFocus','depthOfFieldBlur')), + ('isGroundFlattened',('groundPosition',)),('isGroundReflections',('groundRoughness',))): + enabled = result['scene'][condition].get('value') + for name in names: + if enabled is True: + vector = name in ('centerOfFocus','groundPosition') + fields(result['scene'],'scene',scene,[(name,point if vector else number,'mm' if vector else 'unitless','cm' if vector else None)]) + else: result['scene'][name] = skipped(condition + ' is false or unreadable') + background = result['scene']['backgroundType'].get('value') + if background is not None and background == background_enums['SolidColorRenderSceneBackgroundType'].get('value'): + def color(): + value = scene.backgroundSolidColor + return {name:number(getattr(value,name)) for name in ('red','green','blue','opacity')} + result['scene']['backgroundSolidColor'] = read('scene.backgroundSolidColor',color,'RGBA 0..255; opacity ignored by scene') + result['scene']['backgroundEnvironment'] = skipped('Solid background') + elif background is not None and background == background_enums['EnvironmentRenderSceneBackgroundType'].get('value'): + env, descriptor = object_read('scene.backgroundEnvironment',lambda:scene.backgroundEnvironment) + if env is not None: + fields(descriptor,'scene.backgroundEnvironment',env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)]) + result['scene']['backgroundEnvironment'] = descriptor + result['scene']['backgroundSolidColor'] = skipped('Environment background') + else: + result['errors'].append({'field':'scene.backgroundType','error':'Unknown/unreadable background mode; conditional getters not read'}) + result['scene']['backgroundEnvironment'] = result['scene']['backgroundSolidColor'] = skipped('Unknown background mode') + environments, result['environments']['object'] = object_read('renderManager.renderEnvironments',lambda:manager.renderEnvironments) + if environments is not None: + count = read('environments.count',lambda:number(environments.count)) + result['environments']['count'] = count + n = count.get('value');result['environments']['items'] = [] + if type(n) is int and n >= 0: + result['environments']['truncated'] = n > max_environments + for index in range(min(n,max_environments)): + env, descriptor = object_read('environment.' + str(index),lambda index=index:environments.item(index)) + if env is not None: + fields(descriptor,'environment.'+str(index),env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)]) + result['environments']['items'].append(descriptor) + else: result['errors'].append({'field':'environments.count','error':'Environment count is not a nonnegative integer'}) + result['documentAfter'] = {'modified':read('document.modified.after',lambda:boolean(document.isModified)), + 'saved':read('document.saved.after',lambda:boolean(document.isSaved)), + 'stillActive':read('document.active.after',lambda:boolean(app.activeDocument == document))} + result['documentFlagsUnchanged'] = (result['documentBefore']['modified'].get('available') is True + and result['documentBefore']['saved'].get('available') is True + and result['documentBefore']['modified'] == result['documentAfter']['modified'] + and result['documentBefore']['saved'] == result['documentAfter']['saved'] + and result['documentAfter']['stillActive'].get('value') is True) + classes_complete = all(row.get('available') is True and all(row.get('members',{}).values()) for row in result['classes'].values()) + result['observationComplete'] = not result['errors'] and classes_complete and not result['environments'].get('truncated',False) + need(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES,'Render probe output exceeds byte budget') + return result + + +def owned_script(native_script_factory, *, run_id, host, session_id, document_id): + """Generate only; the caller owns baseline checks and public transport.""" + values = (run_id, host, session_id, document_id) + if not all(isinstance(value,str) and 0 < len(value) <= 256 for value in values): + raise ValueError('Probe identity strings must be nonempty and bounded') + script = native_script_factory('inventory',run_id=run_id,host=host,session_id=session_id,documentId=document_id) + # Reuse the maintained exact-object guard, but do not execute its generic + # inventory/action program: historical rows are unbounded and irrelevant to + # this one-document diagnostic. Future helper changes fail closed in tests. + tree = ast.parse(script) + guards = {} + for node in tree.body: + if isinstance(node,ast.FunctionDef) and node.name in ('require','owned'): + if node.name in guards:raise ValueError('Duplicate native ownership guard') + guards[node.name] = node + if set(guards) != {'require','owned'}: + raise ValueError('Maintained native ownership guards are unavailable') + if ([arg.arg for arg in guards['require'].args.args] != ['condition','message'] + or [arg.arg for arg in guards['owned'].args.args] != ['document_id','allow_closed']): + raise ValueError('Maintained native ownership guard signature changed') + guard_source = '\n'.join(ast.unparse(guards[name]) for name in ('require','owned')) + request = dict(runId=run_id,host=host,sessionId=session_id,documentId=document_id) + source = Path(__file__).read_text().split('\ndef owned_script(',1)[0] + prefix = '''import threading +if threading.current_thread() is not threading.main_thread(): + raise RuntimeError('Native render reads require the main thread') +import builtins, json, platform, re, sys +''' + return prefix + 'REQUEST = json.loads(' + repr(json.dumps(request)) + ')\n' + guard_source + '\n' + source + ''' +require(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host') +require(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID') +modules = [m for n,m in list(sys.modules.items()) + if n.endswith('.mechanical.core') and getattr(m,'SESSION_ID',None) == REQUEST['sessionId']] +require(len(modules) == 1, 'Exact serving mechanical session is unavailable') +registries = getattr(builtins,'_adom_mechanical_ralph_runs',None) +require(isinstance(registries,dict), 'Native ownership registry unavailable') +run = registries.get(REQUEST['runId']) +require(isinstance(run,dict) and run.get('sessionId') == REQUEST['sessionId'], 'Native registry/session missing') +require(isinstance(run.get('documents'),dict), 'Native ownership documents unavailable') +require(isinstance(run.get('baselineObjects'),list) and len(run['baselineObjects']) <= 128, + 'Baseline object inventory exceeds probe budget or is unavailable') +document_count = app.documents.count +require(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget') +row = owned(REQUEST['documentId']) +document = row['object'] +require(app.activeDocument == document, 'Owned render probe document must already be active') +design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType')) +result = {'success':True, 'documentId':REQUEST['documentId'], 'ownerRunId':REQUEST['runId'], + 'sessionId':REQUEST['sessionId'], 'acceptance':'DIAGNOSTIC_ONLY', + 'renderProbe':render_capability_probe(app,document,design)} +require(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES, + 'Whole render diagnostic output exceeds byte budget') +''' diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/test_read_scene_probe.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/test_read_scene_probe.py new file mode 100644 index 0000000..138a8cb --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/test_read_scene_probe.py @@ -0,0 +1,235 @@ +"""No-host fault injection for the bounded read-only render capability probe.""" +import ast +import builtins +import importlib.util +import json +import math +import platform +from pathlib import Path +import sys +import threading +import types +import unittest +from unittest.mock import patch + +import read_scene_probe as probe + +SUPPORT_PATH = Path(__file__).resolve().parents[3] / 'fusion-job-lifecycle-next/tests/ralph_support.py' +spec = importlib.util.spec_from_file_location('render_probe_native_support',SUPPORT_PATH) +support = importlib.util.module_from_spec(spec);spec.loader.exec_module(support) + + +class ReadOnly: + def __init__(self,kind,**values): + object.__setattr__(self,'values',dict(isValid=True,objectType='adsk::fusion::'+kind,**values)) + object.__setattr__(self,'reads',[]) + def __getattr__(self,key): + self.reads.append(key) + if key not in self.values:raise AttributeError(key) + value=self.values[key] + if isinstance(value,Exception):raise value + return value + def __setattr__(self,key,value):raise AssertionError('Native setter called: '+key) + + +def fixture(): + core=types.ModuleType('adsk.core');fusion=types.ModuleType('adsk.fusion');adsk=types.ModuleType('adsk') + core.CameraTypes=types.SimpleNamespace(OrthographicCameraType=0,PerspectiveCameraType=1,PerspectiveWithOrthoFacesCameraType=2) + fusion.RenderSceneBackgroundTypes=types.SimpleNamespace(EnvironmentRenderSceneBackgroundType=0,SolidColorRenderSceneBackgroundType=1) + members={'RenderManager':('rendering','sceneSettings','renderEnvironments','lights','isRenderWorkspaceActive'), + 'Rendering':('startLocalRender','aspectRatio','resolution','resolutionWidth','resolutionHeight','renderQuality','isBackgroundTransparent'), + 'RenderFuture':('filename','imageWidth','imageHeight','progress','renderState','isValid'), + 'SceneSettings':('backgroundType','backgroundEnvironment','backgroundSolidColor','isDepthOfFieldEnabled'), + 'RenderEnvironment':('id','name','isCustomEnvironment')} + def never(*args):raise AssertionError('Native descriptor/mutator invoked') + for name,fields in members.items():setattr(fusion,name,type(name,(),{field:property(never) for field in fields})) + adsk.core,adsk.fusion=core,fusion + point=lambda x,y,z:types.SimpleNamespace(x=x,y=y,z=z) + camera=ReadOnly('Camera',cameraType=1,eye=point(1,2,3),target=point(0,0,0),upVector=point(0,1,0), + isFitView=False,isSmoothTransition=False,perspectiveAngle=.5,getExtents=never) + viewport=ReadOnly('Viewport',camera=camera) + document=ReadOnly('Document',isModified=True,isSaved=False) + design=ReadOnly('Design',parentDocument=document) + rendering=ReadOnly('Rendering',aspectRatio=0,resolution=0,resolutionWidth=320,resolutionHeight=240, + renderQuality=25,isBackgroundTransparent=False,startLocalRender=never) + scene=ReadOnly('SceneSettings',aspectRatio=0,aspectRatioWidth=4.,aspectRatioHeight=3.,backgroundType=1, + backgroundSolidColor=types.SimpleNamespace(red=10,green=20,blue=30,opacity=255), + backgroundEnvironment=RuntimeError('Invalid in solid mode'),brightness=1000.,cameraExposure=-1., + cameraFocalLength=50.,cameraType=1,groundOffset=-.1,lightAngle=math.pi/2, + isDepthOfFieldEnabled=False,isGroundDisplayed=True,isGroundFlattened=False,isGroundReflections=False, + centerOfFocus=RuntimeError('Disabled focus getter'),depthOfFieldBlur=RuntimeError('Disabled blur getter'), + groundPosition=RuntimeError('Disabled ground getter'),groundRoughness=RuntimeError('Disabled reflection getter')) + environment=ReadOnly('RenderEnvironment',id='native-env-id',name='Environment',isCustomEnvironment=False) + environments=ReadOnly('RenderEnvironments',count=2,item=lambda i:environment) + manager=ReadOnly('RenderManager',parentDesign=design,rendering=rendering,sceneSettings=scene, + renderEnvironments=environments,isRenderWorkspaceActive=False) + design.values['renderManager']=manager + app=ReadOnly('Application',activeDocument=document,activeViewport=viewport, + documents=types.SimpleNamespace(count=1,item=lambda i:document)) + return types.SimpleNamespace(app=app,document=document,design=design,manager=manager,rendering=rendering, + scene=scene,camera=camera,viewport=viewport,environments=environments,environment=environment, + modules={'adsk':adsk,'adsk.core':core,'adsk.fusion':fusion},fusion=fusion) + + +class SceneProbe(unittest.TestCase): + def setUp(self): + self.f=fixture();self.p=patch.dict(sys.modules,self.f.modules);self.p.start();self.addCleanup(self.p.stop) + def read(self,**args):return probe.render_capability_probe(self.f.app,self.f.document,self.f.design,**args) + + def test_scalar_units_conditional_getters_and_no_mutation(self): + result=self.read() + self.assertTrue(result['observationComplete']);self.assertEqual(result['errors'],[]) + self.assertFalse(result['sceneWitnessComplete']);self.assertFalse(result['renderStarted']);self.assertFalse(result['settingsWritten']) + self.assertEqual(result['nativeQueueIdle'],'UNVERIFIED') + self.assertEqual(result['camera']['eye']['value'],[10,20,30]) + self.assertEqual(result['scene']['groundOffset']['value'],-1.) + self.assertEqual(result['scene']['lightAngle']['value'],90.) + self.assertEqual(result['scene']['cameraFocalLength']['value'],50.) + self.assertEqual(result['scene']['cameraFocalLength']['nativeUnit'],'mm') + for name in ('backgroundEnvironment','centerOfFocus','depthOfFieldBlur','groundPosition','groundRoughness'): + self.assertNotIn(name,self.f.scene.reads) + self.assertNotIn('getExtents',self.f.camera.reads) + self.assertTrue(result['documentFlagsUnchanged']) + + def test_orthographic_extent_ack_and_active_environment_are_explicit(self): + self.f.camera.values.update(cameraType=0,getExtents=lambda:(True,4.,3.),perspectiveAngle=RuntimeError('Must not read')) + self.f.scene.values.update(backgroundType=0,backgroundEnvironment=self.f.environment) + result=self.read();self.assertEqual(result['camera']['extents']['value'],[40.,30.]) + self.assertNotIn('perspectiveAngle',self.f.camera.reads) + self.assertEqual(result['scene']['backgroundEnvironment']['id']['value'],'native-env-id') + self.f.camera.values['getExtents']=lambda:(False,4.,3.) + self.assertFalse(self.read()['camera']['extents']['available']) + + def test_per_field_errors_do_not_drop_unrelated_observations(self): + self.f.scene.values['brightness']=RuntimeError('unavailable') + self.f.rendering.values['resolutionWidth']=float('nan') + result=self.read();self.assertFalse(result['observationComplete']) + self.assertFalse(result['scene']['brightness']['available']) + self.assertEqual(result['scene']['cameraFocalLength']['value'],50.) + self.assertEqual(result['rendering']['resolutionHeight']['value'],240) + self.assertEqual({e['field'] for e in result['errors']},{'scene.brightness','rendering.resolutionWidth'}) + + def test_missing_class_and_enum_record_availability_without_conditional_guess(self): + del self.f.fusion.RenderFuture;del self.f.fusion.RenderSceneBackgroundTypes + result=self.read();self.assertFalse(result['observationComplete']) + self.assertFalse(result['classes']['RenderFuture']['available']) + self.assertNotIn('backgroundSolidColor',self.f.scene.reads) + self.assertNotIn('backgroundEnvironment',self.f.scene.reads) + + def test_environment_enumeration_strings_and_output_are_bounded(self): + calls=[];self.f.environments.values.update(count=10**9,item=lambda i:calls.append(i) or self.f.environment) + result=self.read(max_environments=3) + self.assertEqual(calls,[0,1,2]);self.assertTrue(result['environments']['truncated']) + self.assertFalse(result['observationComplete']) + self.f.environment.values['name']='x'*10000 + result=self.read(max_environments=1) + self.assertFalse(result['environments']['items'][0]['name']['available']) + self.assertLess(len(json.dumps(result).encode()),probe.MAX_OUTPUT_BYTES) + for bound in (-1,33,True): + with self.assertRaises(RuntimeError):self.read(max_environments=bound) + + def test_wrong_document_or_design_fails_before_renderer_read(self): + self.f.app.values['activeDocument']=object() + with self.assertRaisesRegex(RuntimeError,'identity differs'):self.read() + self.assertNotIn('renderManager',self.f.design.reads) + self.f.app.values['activeDocument']=self.f.document;self.f.design.values['parentDocument']=object() + with self.assertRaises(RuntimeError):self.read() + + def test_foreign_manager_is_not_traversed(self): + self.f.manager.values['parentDesign']=object() + result=self.read();self.assertFalse(result['observationComplete']) + self.assertNotIn('rendering',self.f.manager.reads) + self.assertTrue(result['camera']['eye']['available']) + + def test_worker_thread_does_not_read_native_objects(self): + errors=[] + def run(): + try:self.read() + except RuntimeError as error:errors.append(str(error)) + thread=threading.Thread(target=run);thread.start();thread.join() + self.assertIn('main thread',errors[0]);self.assertEqual(self.f.app.reads,[]) + + def test_scaled_overflow_is_local_error_and_fractional_enum_does_not_select_mode(self): + self.f.scene.values.update(groundOffset=1e308,lightAngle=1e308,backgroundType=1.5) + self.f.camera.values['eye']=types.SimpleNamespace(x=1e308,y=0,z=0) + result=self.read() + self.assertFalse(result['observationComplete']) + for row in (result['scene']['groundOffset'],result['scene']['lightAngle'],result['camera']['eye']): + self.assertFalse(row['available']) + self.assertNotIn('backgroundSolidColor',self.f.scene.reads) + self.assertEqual(result['scene']['cameraFocalLength']['value'],50.) + json.dumps(result,allow_nan=False) + + def test_invalid_document_inventory_refuses_before_render_reads(self): + for count in (True,-1,129,1.5): + self.f.app.values['documents'].count=count + with self.assertRaisesRegex(RuntimeError,'inventory'):self.read() + self.assertNotIn('renderManager',self.f.design.reads) + + def test_owned_script_checks_exact_registry_before_read_and_contains_no_native_mutators(self): + script=probe.owned_script(support.native_script, + run_id='owned-run-123456',host='host',session_id='session',document_id='exact') + compile(script,'owned-render-read','exec') + self.assertLess(script.index("row = owned(REQUEST['documentId'])"),script.index("'renderProbe':render_capability_probe")) + tree=ast.parse(script) + calls=[node.func.attr for node in ast.walk(tree) if isinstance(node,ast.Call) and isinstance(node.func,ast.Attribute)] + for forbidden in ('startLocalRender','activateRenderWorkspace','restoreDefaults','saveAsDefaults','executeTextCommand','activate','close','computeAll'): + self.assertNotIn(forbidden,calls) + setters=[node for node in ast.walk(tree) if isinstance(node,ast.Attribute) and isinstance(node.ctx,ast.Store)] + self.assertEqual(setters,[]) + + def wrapper(self,historical_count=0): + run_id='owned-run-123456';session_id='render-probe-session';document_id='exact-document' + self.f.document.values.update(name='Untitled',creationId='creation') + attrs=types.SimpleNamespace(itemByName=lambda group,key:types.SimpleNamespace(value=run_id if key=='owner' else document_id)) + self.f.design.values['rootComponent']=types.SimpleNamespace(attributes=attrs) + self.f.document.values['products']=types.SimpleNamespace(itemByProductType=lambda kind:self.f.design) + self.f.fusion.Design=types.SimpleNamespace(cast=lambda value:value) + fixture_document=self.f.document + class Documents: + count=1 + def item(self,index):return fixture_document + def __iter__(self):return iter([fixture_document]) + class NoHistoryIteration(dict): + def values(self):raise AssertionError('Historical documents were enumerated') + def items(self):raise AssertionError('Historical documents were enumerated') + self.f.app.values['documents']=Documents() + rows=NoHistoryIteration({document_id:{'object':self.f.document,'documentId':document_id,'name':'Untitled','closed':False}}) + for index in range(historical_count):rows['closed-'+str(index)]={'closed':True,'name':'x'*512} + registry={run_id:{'sessionId':session_id,'documents':rows,'baselineObjects':[]}} + module=types.ModuleType('render_probe_owned.mechanical.core');module.SESSION_ID=session_id + self.p2=patch.dict(sys.modules,{module.__name__:module});self.p2.start();self.addCleanup(self.p2.stop) + script=probe.owned_script(support.native_script,run_id=run_id,host=platform.node(),session_id=session_id,document_id=document_id) + env={'app':self.f.app,'adsk':self.f.modules['adsk']} + return registry,script,env + + def test_real_wrapper_does_not_walk_history_or_return_full_inventory(self): + registry,script,env=self.wrapper(1001) + with patch.object(builtins,'_adom_mechanical_ralph_runs',registry,create=True): + exec(compile(script,'owned-read','exec'),env) + result=env['result'] + self.assertEqual(set(result),{'success','documentId','ownerRunId','sessionId','acceptance','renderProbe'}) + self.assertLessEqual(len(json.dumps(result,allow_nan=False).encode()),probe.MAX_OUTPUT_BYTES) + self.assertTrue(result['renderProbe']['observationComplete']) + + def test_real_wrapper_worker_refuses_before_any_native_ownership_read(self): + registry,script,env=self.wrapper();errors=[] + def execute(): + try:exec(compile(script,'owned-read-worker','exec'),env) + except Exception as error:errors.append(str(error)) + with patch.object(builtins,'_adom_mechanical_ralph_runs',registry,create=True): + worker=threading.Thread(target=execute);worker.start();worker.join(2) + self.assertFalse(worker.is_alive());self.assertIn('main thread',errors[0]) + for obj in (self.f.app,self.f.document,self.f.design):self.assertEqual(obj.reads,[]) + + def test_whole_wrapper_size_is_enforced_after_probe_result(self): + registry,script,env=self.wrapper() + marker="result = {'success':True, 'documentId':REQUEST['documentId']" + self.assertEqual(script.count(marker),1) + script=script.replace(marker,"render_capability_probe = lambda *args: {'extra':'x'*MAX_OUTPUT_BYTES}\n"+marker) + with patch.object(builtins,'_adom_mechanical_ralph_runs',registry,create=True): + with self.assertRaisesRegex(RuntimeError,'Whole render diagnostic'): + exec(compile(script,'oversized-read','exec'),env) + + +if __name__=='__main__':unittest.main() diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_jobs.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_jobs.py new file mode 100644 index 0000000..6479fe2 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_jobs.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""CP8 ordinary durable-operation acceptance. Default plan makes no host calls. + +No restart, crash, storage fault, native busy hold or rendering campaign runs here. +Use exact-owned Ralph registration, checkpoint/reopen, recovery and visual review. +""" +import argparse +import copy +import hashlib +import json +import math +from pathlib import Path +import signal +import sys + +from fusion_harness import evaluate, mechanical_request_digest +from ralph_mechanical import Campaign, recover as shared_recover +from ralph_support import equivalent, native_script + + +EXCLUDED = ("restart_history", "interrupted_native_execution", "disk_or_commit_faults", "busy_native_queue") + +# Explicit outer annotations from the CLI envelope and Rust pass_through / +# etiquette. Native result fields (including errors, targets and durability) +# remain exact. Unknown additions fail comparison instead of being discarded. +BRIDGE_ANNOTATIONS = frozenset(("durationMs", "status", "schemaVersion", "shell", "_contribute", + "_hint", "_hintDocumentHygiene", "_timeoutHint", "documentHygiene", "statusVerb", "timeoutSeconds", + "windowEtiquette", "_timing")) + + +def native_envelope(response, expected=None): + """Validate every envelope before comparing its native semantic fields.""" + if expected is None and response.get('success') is False: + expected = response.get('errorCode') + errors = evaluate(response, {'errorCode':expected} if expected else {'ok':True}) + if errors: raise ValueError('Invalid public result envelope: ' + '; '.join(errors)) + def project(value): + result = {key:item for key,item in value.items() + if key not in BRIDGE_ANNOTATIONS and key not in ('replayed', 'output')} + # Actual CLI replies duplicate the native response in output. Validate + # that duplicate too: a conflicting data value is not just decoration. + if value.get('output') not in (None, ''): + nested = value['output'] + if isinstance(nested, str): nested = json.loads(nested) + # The CLI adds error:"" to successful outer replies even when + # the native duplicate has no error field. Only that proven + # envelope addition is removable; native errors remain exact. + if (isinstance(nested, dict) and value.get('success') is True + and nested.get('success') is True and result.get('error') == '' + and 'error' not in nested): + result.pop('error') + if (not isinstance(nested, dict) or + json.dumps(project(nested), sort_keys=True, allow_nan=False) != + json.dumps(result, sort_keys=True, allow_nan=False)): + raise ValueError('CLI output conflicts with the native response') + return result + return project(response) + + +def native_signature(response, expected=None): + # Python dictionary equality treats True, 1 and 1.0 as equal. Persisted + # result comparison retains the exact JSON types instead. + return json.dumps(native_envelope(response, expected), sort_keys=True, separators=(',', ':'), allow_nan=False) + + +def box_depth_parameter(inspection): + # The owned new_design box creates a named user parameter plus a model + # parameter driven by it; both can have the same numerical value. + matches = [p for p in inspection['parameters'] if p.get('name') == 'depth'] + if len(matches) != 1 or matches[0].get('unit') != 'mm' or not equivalent(matches[0].get('value'), 10): + raise ValueError('Owned box depth parameter identity, units or value differ') + return matches[0] + + +def terminal_contract(request, response, status, source_version, observed_document): + """Independent public-history assertions; no production module imports.""" + errors = [] + def need(condition, message): + if not condition: errors.append(message) + sid, oid = request['sessionId'], request['operationId'] + phase = 'succeeded' if response.get('success') is True else 'failed' + need(status.get('success') is True and status.get('sessionId') == sid and status.get('operationId') == oid, + 'Status identity does not match the submitted operation') + need(status.get('currentSessionId') == sid and status.get('historical') is False, + 'Ordinary status unexpectedly changed session') + need(status.get('state') == phase and status.get('phase') == phase and status.get('outcome') == 'recorded' + and status.get('reconciliationRequired') is False, 'Status is not the requested terminal outcome') + history = status.get('history') or {} + need(history.get('sessionId') == sid and history.get('operationId') == oid + and history.get('operation') == request['op'] and history.get('sourceVersion') == source_version + and history.get('storageVersion') == 1 and history.get('phase') == phase, 'History source/identity/phase differs') + need(history.get('requestDigest') == mechanical_request_digest(request), 'Canonical request digest differs') + need(history.get('requestedTarget') == request.get('target'), 'Recorded requested target differs') + observed = history.get('observedDocument') or {} + need(observed.get('documentId') == observed_document, 'Observed native document differs') + need('creationId' in observed and 'dataFileId' in observed, 'Observed native identity provenance is incomplete') + events = history.get('events') or [] + need([row.get('phase') for row in events] == ['admitted', 'dispatched', 'executing', phase], + 'Durable phase sequence differs') + times = [history.get('createdAt')] + [row.get('at') for row in events] + [history.get('updatedAt')] + need(all(type(t) in (int, float) and math.isfinite(t) and t > 0 for t in times), 'Invalid phase timestamps') + if all(type(t) in (int, float) and math.isfinite(t) for t in times): + need(times == sorted(times), 'Phase timestamps are not ordered') + for label, value in (('initial', response), ('recorded', status.get('result') or {})): + durability = value.get('durability') or {} + need(value.get('sessionId') == sid and value.get('operationId') == oid, + label + ' result identity differs') + need(durability.get('terminalRecorded') is True and durability.get('recordedSessionId') == sid + and durability.get('storageVersion') == 1 and durability.get('phase') == phase, + label + ' result is not durably terminal') + for label, value in (('initial', response), ('recorded', status.get('result') or {}), ('status', status)): + started = value.get('nativeExecutionStarted') + need(started is None or started is True, label + ' claims this ordinary executed operation did not execute') + try: + need(native_signature(response) == native_signature(status.get('result') or {}), + 'Recorded terminal result differs from acknowledged result') + except (ValueError, TypeError) as error: + errors.append(str(error)) + return errors + + +class DurableOperations(Campaign): + def __init__(self, args, client=None): + super().__init__(args, client) + snapshots = self.directory / ('harness-' + self.id) + snapshots.mkdir(parents=True, exist_ok=False) + self.report['harnessSources'] = [] + for name in ('ralph_jobs.py', 'ralph_mechanical.py', 'ralph_support.py', 'fusion_harness.py'): + data = Path(__file__).with_name(name).read_bytes() + copied = snapshots / name; copied.write_bytes(data) + self.report['harnessSources'].append({'name':name, 'path':str(copied), 'sha256':hashlib.sha256(data).hexdigest()}) + self.report['acceptanceScope'] = {'ordinary':'REQUESTED', **{name:'NOT_RUN' for name in EXCLUDED}} + self.report['limitations'] = ['Ordinary synchronous durable writes only; no native restart/crash/fault inference.', + 'Native, archive/cleanup and hash-bound visual review remain separate.'] + + def native(self, action, **values): + if action != 'document_set': return super().native(action, **values) + if self.ambiguous: raise RuntimeError('Unresolved operation prevents native lifecycle observations') + script = native_script('inventory', run_id=self.id, host=self.args.host_name, session_id=self.session) + script += ''' +labels = [] +for document in app.documents: + matches = ['baseline:' + str(i) for i, original in enumerate(run['baselineObjects']) if document == original] + matches += ['owned:' + row['documentId'] for row in run['documents'].values() if not row['closed'] and document == row['object']] + require(len(matches) <= 1, 'Native document ownership is ambiguous') + labels.append(matches[0] if matches else 'unowned:' + document.creationId) +result['openDocuments'] = sorted(labels) +''' + response = self.call('fusion_run_modeling_script', {'script':script}) + result = response['data']['result'] + if result.get('success') is not True: raise RuntimeError('Native document-set observation failed') + return result + + def dispatch(self, request, expected=None): + if self.ambiguous: raise RuntimeError('Previous operation is unresolved; no additional dispatch') + request = copy.deepcopy(request) + self.last_operation = request['operationId'] + self.owned.manifest['lastDispatch'] = copy.deepcopy(request) + self.owned.persist() + return self.call('fusion_mechanical', request, mutating=True, expected=expected) + + def request(self, label, op, **values): + result = dict(op=op, sessionId=self.session, operationId=self.id + '-' + label, **values) + if op != 'new_design': result.setdefault('target', copy.deepcopy(self.target)) + return result + + def history(self, request, response, observed_document): + expected = response.get('errorCode') if response.get('success') is False else None + status = self.call('fusion_mech_operation_status', + {'sessionId':self.session, 'operationId':request['operationId']}, expected=expected) + errors = terminal_contract(request, response, status, self.args.expect_addin, observed_document) + self.check(request['op'] + ' exact durable terminal contract', not errors, {'errors':errors, 'status':status}) + return native_signature(status, expected) + + def ordinary(self): + caps = self.call('fusion_mech_capabilities') + durable = caps.get('idempotency', {}) + self.check('Durable history capability is explicit', caps.get('sessionId') == self.session + and durable.get('storageVersion') == 1 and durable.get('capacity') == 10000 + and durable.get('databaseByteLimit') == 64 * 1024 * 1024 + and durable.get('recordByteLimit') == 4 * 1024 * 1024 + and durable.get('admission') == 'FULL SQLite commit before native dispatch', durable) + prior = self.call('fusion_mechanical', {'op':'inspect'})['data']['target']['documentId'] + before_documents = self.native('document_set')['openDocuments'] + creation = self.request('create', 'new_design', name='Untitled', box={'width':20,'height':30,'depth':10}) + result = self.dispatch(creation) + data = result['data']; self.document_id = data['target']['documentId']; self.target = data['target'] + # Register exact returned native identity before fragile acceptance checks. + self.owned.manifest.setdefault('pendingRegistrations', {})[self.document_id] = data + self.owned.persist() + self.bind_document(self.native('register', documentId=self.document_id, role=self.fixture)) + del self.owned.manifest['pendingRegistrations'][self.document_id]; self.owned.persist() + self.inspect() + if self.args.inject_failure == 'after-new': raise RuntimeError('Requested owned registration diagnostic') + created = self.measured('Created box', 6000) + self.check('Creation source and result are distinct exact documents', prior != self.document_id) + self.check('Exactly one document created', self.native('document_set')['openDocuments'] == sorted(before_documents + ['owned:' + self.document_id])) + original_status = self.history(creation, result, prior) + documents = self.native('document_set')['openDocuments'] + replay = self.dispatch(creation) + self.check('Creation replay is byte-equivalent semantic result', replay.get('replayed') is True and native_signature(replay) == native_signature(result), replay) + self.check('Creation replay creates no document or geometry', documents == self.native('document_set')['openDocuments'] and equivalent(created, self.witness())) + changed = copy.deepcopy(creation); changed['box']['depth'] = 11 + conflict = self.dispatch(changed, expected='operation_conflict') + self.check('Changed digest refuses without model/document effects', conflict.get('errorCode') == 'operation_conflict' + and documents == self.native('document_set')['openDocuments'] and equivalent(created, self.witness())) + self.check('Conflict preserves original durable record', self.history(creation, result, prior) == original_status) + + inspection = self.inspect() + parameter = box_depth_parameter(inspection) + self.check('Owned box depth identity, units and initial value match', True, parameter) + depth = parameter['name'] + edit = self.request('depth', 'parameter_set', name=depth, expression='20 mm') + edited = self.dispatch(edit) + self.inspect(); after = self.measured('Edited box', 12000) + edited_status = self.history(edit, edited, self.document_id) + transport_changed = {**edit, 'caller':{'sessionId':'ralph-envelope-only'}, '_callerBudgetMs':60000} + repeated = self.dispatch(transport_changed) + self.check('Transport metadata preserves canonical identity', repeated.get('replayed') is True + and native_signature(repeated) == native_signature(edited), repeated) + self.check('Parameter replay preserves exact geometry and terminal history', equivalent(after, self.witness()) + and self.history(edit, edited, self.document_id) == edited_status) + + self.inspect() + failing = self.request('abort', 'parameter_batch', changes=[ + {'action':'set','name':depth,'expression':'15 mm'}, + {'action':'set','name':depth,'expression':'MissingDurableDepth + 1 mm'}]) + failure = self.dispatch(failing, expected='mutation_failed') + self.check('Intended atomic failure verifies recovery', failure.get('causeCode') == 'invalid_expression' + and failure.get('recovery', {}).get('verified') is True, failure) + self.check('Independent native geometry/parameter witness restored', equivalent(after, self.witness())) + failed_status = self.history(failing, failure, self.document_id) + replay_failure = self.dispatch(failing, expected='mutation_failed') + self.check('Failed operation replay remains the original failure', replay_failure.get('replayed') is True + and native_signature(replay_failure) == native_signature(failure)) + self.check('Failed replay preserves witness and immutable terminal record', equivalent(after, self.witness()) + and self.history(failing, failure, self.document_id) == failed_status) + self.inspect() + self.report['acceptanceScope']['ordinary'] = 'PASS' + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('mode', choices=('plan','run','recover'), nargs='?', default='plan') + for name in ('target','host-name','evidence','expect-bridge','expect-addin','shotlog-channel'): + parser.add_argument('--' + name) + parser.add_argument('--inject-failure', choices=('after-new','after-model')) + args = parser.parse_args(argv) + if args.mode == 'plan': + print(json.dumps({'hostCalls':False, 'campaign':'ordinary', 'notRun':list(EXCLUDED), + 'checks':['public durable admission/status','exact creation and parameter replay','changed-payload conflict', + 'failed atomic update and immutable failure replay','native geometry and F3D roundtrip','exact owned cleanup'], + 'visualReview':'Use ralph_mechanical.py review with hash-bound annotations after native completion.'}, indent=2)) + return 0 + if not all((args.target,args.host_name,args.evidence)): parser.error('Explicit target, host and evidence are required') + if args.mode == 'recover': return shared_recover(args, campaign_type=DurableOperations) + if not all((args.expect_bridge,args.expect_addin)): parser.error('Explicit expected versions are required') + directory = Path(args.evidence) + if directory.exists(): parser.error('Evidence directory must be new') + directory.mkdir(parents=True) + args.fixtures, args.iterations = ['ordinary'], 1 + campaign = DurableOperations(args) + def interrupt(signum, _): raise KeyboardInterrupt('Signal ' + str(signum)) + signal.signal(signal.SIGTERM, interrupt); signal.signal(signal.SIGINT, interrupt) + code = campaign.run() + print(json.dumps({'overall':campaign.report['overall'], 'report':str(directory / 'report.json')}, indent=2)) + return code + + +if __name__ == '__main__': sys.exit(main()) diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_mechanical.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_mechanical.py new file mode 100644 index 0000000..1c2b14f --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_mechanical.py @@ -0,0 +1,614 @@ +#!/usr/bin/env python3 +"""Bounded background Fusion mechanical campaign using the public bridge CLI. + +Examples (no host calls are made by plan or review): + python3 tests/ralph_mechanical.py plan + python3 tests/ralph_mechanical.py baseline --target MACHINE --host-name MACHINE --evidence DIR + python3 tests/ralph_mechanical.py run --target MACHINE --host-name MACHINE --evidence NEW_DIR \ + --expect-bridge VERSION --expect-addin VERSION --iterations 1 + python3 tests/ralph_mechanical.py recover --evidence EXISTING_DIR --target MACHINE --host-name MACHINE + python3 tests/ralph_mechanical.py review --evidence DIR --reviewer NAME --annotations review.json + +review.json is a list of {sha256, verdict: PASS|FAIL, notes}. A successful native +run exits 0 but remains VISUAL_PENDING until every image is explicitly reviewed. +The runner sends no runtime lifecycle commands or foreground input, and requires +a running baseline. The underlying transport may still autoheal between calls; +session and ownership guards refuse continuation if that changes the runtime. +""" +import argparse +import base64 +import copy +import hashlib +import json +import math +from pathlib import Path +import signal +import subprocess +import sys +import time +import uuid + +from fusion_harness import BridgeClient, CallResult, OwnedRun, data_payload, evaluate, observe, run_id +from ralph_support import (BASELINE_SCRIPT, artifact, equivalent, health_identity_request, + health_probe_script, native_script, validate_live_identity, visual_review, write_json) + + +PLAN = { + "fixtures": { + "bracket": "Constrained plate with through hole; parameter changes, failed atomic batch, F3D reopen and viewport.", + "assembly": "Two shared blocks and one independent copy; source edit, placement, native mass, BOM/CSV and F3D.", + "supplier": "Optional explicit local supplier CAD and SHA/provenance; repeated instances, BOM and F3D."}, + "requiredVerbs": ["fusion_state", "fusion_addin_status", "bridge_list", "run_script", "fusion_document_info", "fusion_get_preferences", + "fusion_mech_capabilities", "fusion_mechanical", "fusion_mech_operation_status", "fusion_run_modeling_script"], + "constraints": ["Explicit target, native host name, and expected versions; runtime must already be ready.", + "Requires an existing active Design; refuses a zero-document baseline to avoid implicit creation.", + "Every typed mutation dispatched once; ambiguous results reconcile by operation status only.", + "Exact live document IDs plus native object/marker/checkpoint guards; creationId is evidence only.", + "Preferences and pins are observed and compared; no preference setters or runtime lifecycle calls.", + "One to five iterations. Native viewport evidence requires separate SHA-bound review.", + "Interrupted or changed-session cleanup retains documents rather than inferring ownership."], + "scope": "Representative maintained fixtures, not exhaustive feature/options or application UI coverage."} + + +class Campaign: + def __init__(self, args, client=None): + self.args = args + self.directory = Path(args.evidence).resolve() + self.id = run_id() + self.client = client or BridgeClient(args.target, self.id, ai_thread="Fusion mechanical Ralph", evidence_dir=self.directory / "calls") + self.report = {"runId": self.id, "target": args.target, "host": args.host_name, + "native": "RUNNING", "cleanup": "NOT_STARTED", "visual": {"status": "PENDING"}, + "overall": "RUNNING", "checks": [], "artifacts": [], "fixtures": [], + "requestedFixtures": ["%02d-%s" % (i, f) + for i in range(1, getattr(args, "iterations", 0) + 1) + for f in getattr(args, "fixtures", [])], + "limitations": PLAN["scope"]} + self.session = None + self.target = None + self.owned = None + self.document_id = None + self.fixture = None + self.ambiguous = False + self.last_operation = None + + def persist(self): + write_json(self.directory / "report.json", self.report) + + def check(self, label, condition, evidence=None): + row = {"label": label, "status": "PASS" if condition else "FAIL", "evidence": evidence} + self.report["checks"].append(row) + self.persist() + if not condition: + raise RuntimeError(label) + + def call(self, verb, args=None, *, mutating=False, expected=None, timeout=190): + result = self.client.call(verb, args, timeout=timeout, mutating=mutating) + if result.ambiguous: + self.ambiguous = True + if verb == "fusion_mechanical" and args and args.get("operationId"): + reconciled = self.client.reconcile_mechanical(result, self.session, args["operationId"], polls=8, interval=1) + write_json(self.directory / ("reconcile-" + args["operationId"] + ".json"), reconciled) + if reconciled["state"] in ("succeeded", "failed"): + self.ambiguous = False + raw = reconciled["result"] + else: + raise RuntimeError("Unresolved mutation; no replay and no dependent cleanup") + else: + raise RuntimeError("Uncertain native lifecycle action; use recover to inspect its registry") + else: + raw = result.raw + errors = evaluate(raw, {"errorCode": expected} if expected else {"ok": True}) + if errors: + raise RuntimeError(verb + ": " + "; ".join(errors)) + payload = observe(raw).payload + return payload + + def native(self, action, **values): + if self.ambiguous: + raise RuntimeError("Unresolved operation prevents native lifecycle actions") + script = native_script(action, run_id=self.id, host=self.args.host_name, session_id=self.session, **values) + response = self.call("fusion_run_modeling_script", {"script": script}, + mutating=action in {"begin", "register", "activate", "close", "checkpoint", "capture", "restore"}) + result = response["data"]["result"] + if result.get("success") is not True: + raise RuntimeError("Native adapter returned no explicit success") + return result + + def runtime_observation(self, require_versions=False): + state = self.call("fusion_state", {"screenshot": False, "nativeViewport": False}) + addin = self.call("fusion_addin_status") + bridges = self.call("bridge_list") + runtime = next((x for x in bridges.get("bridges", []) if x.get("name") == "fusion360"), None) + if not runtime: + raise RuntimeError("Fusion runtime is absent from target bridge inventory") + # Guard before pass-through queries: the transport can launch Fusion + # for a cold application, and raw scripts create an implicit document. + if not (state.get("running") is True and state.get("addinResponding") is True and + not state.get("mainThreadBusy") and not state.get("blockingDialogs") and + runtime.get("paused") is False and not runtime.get("entryMismatch")): + raise RuntimeError("Fusion must already be running, responsive, idle and free of blocking dialogs") + if require_versions and not (addin.get("addinStale") is False and addin.get("version") == self.args.expect_addin): + raise RuntimeError("Expected runtime versions differ; no native pass-through permitted") + request = health_identity_request(runtime, self.args.host_name) + response = self.call("run_script", {"interpreter": "powershell", + "scriptB64": base64.b64encode(health_probe_script(request).encode()).decode(), "timeoutSeconds": 30, + "reason": "Read the discovered Fusion bridge process and its loopback health version without changing runtime state"}, timeout=40) + if response.get("exitCode") != 0 or response.get("timedOut") is True: + raise RuntimeError("Read-only native bridge identity probe failed: " + str(response.get("stderr", ""))) + identity = validate_live_identity(request, json.loads(response["stdout"]), + self.args.expect_bridge if require_versions else None, self.args.expect_addin if require_versions else None) + runtime = {**runtime, "liveIdentity": identity} + write_json(self.directory / "live-runtime-identity.json", {"reportedRunningVersion": runtime.get("runningVersion"), + "installedVersion": runtime.get("installedVersion"), "expectedIdentity": request, "observed": identity}) + return state, addin, runtime + + def baseline(self, require_versions=False): + state, addin, runtime = self.runtime_observation(require_versions) + document_info = self.call("fusion_document_info") + # This verb's raw script path auto-creates a design when no docs exist. + # Confirm a nonempty document inventory before using even a read script. + payload = data_payload(observe(document_info)) + docs = payload.get("openDocuments", payload.get("documents")) + if not isinstance(docs, list) or not docs: + raise RuntimeError("No open document inventory; native baseline cannot remain read-only") + native = self.call("fusion_run_modeling_script", {"script": BASELINE_SCRIPT})["data"]["result"] + if native["host"].lower() != self.args.host_name.lower(): + raise RuntimeError("Native host differs from --host-name") + if native.get("activeProductType") != "DesignProductType": + raise RuntimeError("Baseline requires an existing active Design product") + preferences = self.call("fusion_get_preferences").get("current") + baseline = {"state": state, "addin": addin, "runtime": {k: runtime.get(k) for k in + ("name", "devPinned", "paused", "runningVersion", "installedVersion", "entryMismatch", "instanceCount")}, + "liveIdentity": runtime["liveIdentity"], "preferences": preferences, "native": native} + write_json(self.directory / "baseline.json", baseline) + if require_versions: + if not (state.get("running") is True and state.get("addinResponding") is True and + not state.get("mainThreadBusy") and not state.get("blockingDialogs") and + addin.get("addinStale") is False and addin.get("version") == self.args.expect_addin): + raise RuntimeError("Readiness or expected runtime version differs; no mutation permitted") + if runtime["liveIdentity"]["health"]["bridgeVersion"] != self.args.expect_bridge: + raise RuntimeError("Actual native bridge does not match the requested version") + return baseline + + def mechanical(self, op, *, write=False, expected=None, **values): + if self.ambiguous: + raise RuntimeError("Previous operation is unresolved") + args = {"op": op, **values} + if op not in ("new_design", "inspect"): + args.setdefault("target", self.target) + if write: + args.update(sessionId=self.session, operationId=self.id + "-" + uuid.uuid4().hex[:12]) + self.last_operation = args["operationId"] + if self.owned: + self.owned.manifest["lastDispatch"] = copy.deepcopy(args) + self.owned.persist() + result = self.call("fusion_mechanical", args, mutating=write, expected=expected) + if result.get("sessionId") != self.session: + raise RuntimeError("Mechanical session changed") + fresh = result.get("data", {}).get("target", result.get("target")) + if fresh: + if self.document_id and op != "new_design" and fresh["documentId"] != self.document_id: + raise RuntimeError("Unexpected runtime document identity") + self.target = fresh + return result if expected else result["data"] + + def inspect(self): + data = self.mechanical("inspect") + if not data.get("complete"): + raise RuntimeError("Mechanical inspection is incomplete") + return data + + def entity(self, ref): + return self.mechanical("inspect_entity", entity=ref)["entity"] + + def sketch(self, name): + row = next(x for x in self.inspect()["sketches"] if x["name"] == name) + return self.entity(row["ref"]) + + def component(self, name): + return next(x for x in self.inspect()["components"] if x["name"] == name) + + def witness(self): + return self.native("witness", documentId=self.document_id)["witness"] + + def bind_document(self, row): + doc_id = row["documentId"] + if doc_id not in self.owned.manifest["documents"]: + self.owned.register_document(doc_id, row["role"], metadata={k: row.get(k) for k in ("creationId", "name", "closed")}) + else: + self.owned.manifest["documents"][doc_id]["metadata"]["closed"] = row.get("closed") + self.owned.persist() + self.owned.bind_document_cleanup(doc_id, lambda identity: self.native("close", documentId=identity), + lambda identity: self.native("verify", documentId=identity)) + if row.get("checkpoint"): + cp = row["checkpoint"] + self.owned.mark_checkpoint(doc_id, cp["path"], cp["sha256"], verified=True) + + def new(self, fixture): + self.fixture = fixture + self.document_id = None + data = self.mechanical("new_design", write=True, name="Ralph " + self.id + " " + fixture) + self.document_id = data["target"]["documentId"] + # Persist the public creation result before native registration, so an + # interrupted register remains visible instead of an untracked doc. + self.owned.manifest.setdefault("pendingRegistrations", {})[self.document_id] = data + self.owned.persist() + row = self.native("register", documentId=self.document_id, role=fixture) + self.bind_document(row) + del self.owned.manifest["pendingRegistrations"][self.document_id] + self.owned.persist() + self.inspect() # Ownership attributes conservatively change revision. + if self.args.inject_failure == "after-new": + raise RuntimeError("Requested cleanup diagnostic after owned document creation") + + def measured(self, label, volume, instances=1): + native = self.witness() + inspection = self.inspect() + self.check(label + " complete healthy timeline", inspection["health"]["healthy"] is True and inspection["health"]["complete"] is True, inspection["health"]) + public = self.mechanical("assembly_properties") + self.check(label + " native volume", equivalent(native["physical"]["volume"], volume, 2e-5), native) + self.check(label + " independent instance volume", equivalent(native["instanceVolume"], volume, 2e-5), native["instanceVolume"]) + self.check(label + " native instance count", native["instanceCount"] == instances, native["instanceCount"]) + self.check(label + " public and native physical properties", all(equivalent(public[k], native["physical"][k]) for k in ("volume", "mass", "centerOfMass")), public) + return native + + def create_sketch(self, name, items, plane="xy", component=None): + self.inspect() + values = {"name": name, "plane": plane} + if component: values["component"] = component + sk = self.mechanical("sketch_create", write=True, **values)["sketch"] + return self.mechanical("sketch_geometry", write=True, sketch=sk["ref"], items=items)["sketch"] + + def bracket(self): + self.new(self.fixture) + self.mechanical("parameter_batch", write=True, changes=[{"action": "create", "name": n, "expression": v, "unit": "mm"} + for n, v in (("Width", "40 mm"), ("Height", "20 mm"), ("Thickness", "8 mm"))]) + self.create_sketch("Bracket outline", [{"type": "rectangle", "start": [0, 0], "end": [40, 20]}]) + for index in range(4): + sk = self.sketch("Bracket outline") + line = sk["curves"][index] + a, b = line["startSketchPoint"]["position"], line["endSketchPoint"]["position"] + kind = "horizontal" if abs(a[1] - b[1]) < 1e-7 else "vertical" + self.mechanical("sketch_constraint", write=True, sketch=sk["ref"], type=kind, entities=[line["ref"]]) + sk = self.sketch("Bracket outline") + self.mechanical("sketch_constraint", write=True, sketch=sk["ref"], type="fix", entities=[sk["curves"][0]["startSketchPoint"]["ref"]]) + for kind, expression, axis, position in (("horizontal", "Width", 1, [20, -5]), ("vertical", "Height", 0, [45, 10])): + sk = self.sketch("Bracket outline") + line = next(c for c in sk["curves"] if abs(c["startSketchPoint"]["position"][axis] - c["endSketchPoint"]["position"][axis]) < 1e-7) + self.mechanical("sketch_dimension", write=True, sketch=sk["ref"], type=kind, + entities=[line["startSketchPoint"]["ref"], line["endSketchPoint"]["ref"]], expression=expression, position=position) + sk = self.sketch("Bracket outline") + self.check("Bracket sketch is fully constrained", sk["fullyConstrained"] is True) + self.mechanical("feature_create", write=True, type="extrude", name="Bracket solid", profiles=[sk["profiles"][0]["ref"]], distance="Thickness") + self.measured("Bracket before hole", 6400) + plane = self.mechanical("plane_create", write=True, type="offset", plane="xy", offset="Thickness", name="Hole plane")["plane"]["ref"] + self.create_sketch("Hole point", [{"type": "point", "position": [20, 10]}], plane=plane) + data = self.inspect() + sk = self.sketch("Hole point") + point = next(p for p in sk["points"] if equivalent(p["position"][:2], [20, 10])) + self.mechanical("feature_create", write=True, type="hole", name="Mounting hole", points=[point["ref"]], diameter=6, depth="Thickness", bodies=[{**data["bodies"][0]["ref"], **self.target}]) + self.measured("Bracket with mounting hole", 6400 - math.pi * 9 * 8) + self.mechanical("parameter_batch", write=True, changes=[{"action": "set", "name": "Width", "expression": "50 mm"}, {"action": "set", "name": "Thickness", "expression": "10 mm"}]) + before = self.measured("Bracket after parameter edit", 10000 - math.pi * 9 * 10) + self.inspect() + failure = self.mechanical("parameter_batch", write=True, expected="mutation_failed", changes=[ + {"action": "set", "name": "Width", "expression": "60 mm"}, + {"action": "set", "name": "Thickness", "expression": "NoSuchParameter + 1 mm"}]) + self.check("Failed parameter batch confirms recovery", failure.get("recovery", {}).get("verified") is True, failure) + self.check("Failed parameter batch has the intended cause", failure.get("causeCode") == "invalid_expression", failure) + self.check("Failed parameter batch restores native geometry", equivalent(before, self.witness())) + self.inspect() + status = self.call("fusion_mech_operation_status", {"sessionId": self.session, "operationId": self.last_operation}, expected="mutation_failed") + self.check("Failed operation has a terminal ledger result", status.get("state") == "failed", status) + + def assembly(self): + self.new(self.fixture) + self.mechanical("component_create", write=True, name="Shared", transform={"position": [0, 0, 0]}) + component = self.component("Shared") + self.create_sketch("Block section", [{"type": "rectangle", "start": [0, 0], "end": [10, 10]}], component=component["ref"]) + before = {p["name"] for p in self.inspect()["parameters"]} + sk = self.sketch("Block section") + component = self.component("Shared") + self.mechanical("feature_create", write=True, type="extrude", name="Block depth", component=component["ref"], profiles=[{**sk["profiles"][0]["ref"], **self.target}], distance=10) + parameters = [p for p in self.inspect()["parameters"] if p["name"] not in before and p["unit"] == "mm" and equivalent(p["value"], 10)] + self.check("Assembly driving depth is unambiguous", len(parameters) == 1, parameters) + component = self.component("Shared") + self.mechanical("occurrence_insert", write=True, component=component["ref"], transform={"position": [30, 0, 0]}) + component = self.component("Shared") + copy_data = self.mechanical("occurrence_insert", write=True, component=component["ref"], independentCopy=True, transform={"position": [60, 0, 0]})["occurrence"] + self.check("Independent copy has a separate definition", copy_data["component"]["id"] != component["id"]) + self.mechanical("component_metadata", write=True, component=copy_data["component"]["ref"], changes={"name": "Independent", "partNumber": "RALPH-COPY"}) + component = self.component("Shared") + self.mechanical("component_metadata", write=True, component=component["ref"], changes={"partNumber": "RALPH-SHARED", "description": "Repeated block fixture"}) + self.inspect() + self.mechanical("parameter_set", write=True, name=parameters[0]["name"], expression="20 mm") + native = self.measured("Repeated assembly", 5000, 3) + self.check("Shared edit leaves independent copy unchanged", equivalent(sorted(sum(b["volume"] for b in c["bodies"]) for c in native["components"] if c["name"] != ""), [1000, 2000]), native["components"]) + self.check("All occurrence placements persist after recompute", equivalent(sorted([o["transform"][i] for i in (3, 7, 11)] for o in native["occurrences"]), [[0, 0, 0], [30, 0, 0], [60, 0, 0]]), native["occurrences"]) + self.verify_bom(5000, {"RALPH-SHARED": 2, "RALPH-COPY": 1}) + + def supplier(self): + self.new(self.fixture) + provenance = {"supplier": self.args.supplier_name, "supplierPartNumber": self.args.supplier_part, "sourceUrl": self.args.supplier_url} + self.mechanical("cad_import", write=True, filename=self.args.supplier_file, sha256=self.args.supplier_sha256, + name="Purchased", provenance=provenance) + comp = self.component("Purchased") + self.mechanical("component_metadata", write=True, component=comp["ref"], changes={"asPurchased": True, "partNumber": self.args.supplier_part}) + comp = self.component("Purchased") + self.mechanical("occurrence_insert", write=True, component=comp["ref"], transform={"position": [50, 0, 0]}) + native = self.witness() + self.check("Supplier import contains positive native volume", native["physical"]["volume"] > 0, native) + bom = self.verify_bom(native["physical"]["volume"], {self.args.supplier_part: 2}) + part = next(p for p in bom["parts"] if p["partNumber"] == self.args.supplier_part) + self.check("Supplier BOM preserves checksum and provenance", all(part.get(k) == v for k, v in {**provenance, "sha256": self.args.supplier_sha256.lower()}.items()), part) + + def verify_bom(self, volume, quantities): + self.inspect() + bom = self.mechanical("bom") + self.check("BOM has one row per expected definition", len(bom["parts"]) == len(quantities) and len({p["partNumber"] for p in bom["parts"]}) == len(quantities), bom["parts"]) + self.check("BOM quantities match actual instances", {p["partNumber"]: p["quantity"] for p in bom["parts"]} == quantities, bom) + self.check("BOM physical total agrees with native instances", equivalent(bom["physical"]["volume"], volume), bom["physical"]) + for mode, expected in (("flat", bom["parts"]), ("structured", bom["instances"])): + self.inspect() + path = self.owned.manifest["hostScratch"] + "/" + self.fixture + "-" + mode + ".csv" + self.mechanical("bom_export", write=True, filename=path, mode=mode) + out = self.native("file", path=path, csv=True) + rows = out["rows"] + self.check(mode + " CSV row count", len(rows) == len(expected), rows) + if mode == "flat": + self.check("Flat CSV definitions are unique", len(rows) == len(quantities) and len({x["partNumber"] for x in rows}) == len(quantities), rows) + self.check("Flat CSV quantities", {x["partNumber"]: int(x["quantity"]) for x in rows} == quantities, rows) + else: + self.check("Structured CSV hierarchy", [(x["path"], x["parentPath"], int(x["depth"])) for x in rows] == [(x["path"], x["parentPath"] or "", x["depth"]) for x in expected], rows) + self.save_artifact(out, "bom") + return bom + + def save_artifact(self, out, kind): + data = base64.b64decode(out["base64"], validate=True) + if hashlib.sha256(data).hexdigest() != out["sha256"]: + raise RuntimeError("Transferred artifact hash differs from host") + path = self.directory / "artifacts" / Path(out["path"].replace("\\", "/")).name + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + raise RuntimeError("Local artifact destination already exists") + path.write_bytes(data) + item = {**artifact(path, out["sha256"]), "kind": kind, "hostPath": out["path"], "fixture": self.fixture} + self.owned.register_file(out["path"], out["sha256"], retain=True) + self.report["artifacts"].append(item) + if kind == "viewport" and self.args.shotlog_channel: + process = subprocess.run(["shotlog", "inject", "--channel", self.args.shotlog_channel, "--native", "--desc", self.fixture, + "--source", "Fusion native viewport", str(path)], capture_output=True, text=True, timeout=30) + item["shotlog"] = {"returncode": process.returncode, "stdout": process.stdout, "stderr": process.stderr} + if process.returncode: + raise RuntimeError("Requested shotlog injection failed") + self.persist() + + def checkpoint(self, capture=True): + self.inspect() + stem = self.fixture + "-" + uuid.uuid4().hex[:8] + path = self.owned.manifest["hostScratch"] + "/" + stem + ".f3d" + if capture: + image_path = self.owned.manifest["hostScratch"] + "/" + stem + ".png" + self.native("capture", documentId=self.document_id, path=image_path) + self.save_artifact(self.native("file", path=image_path), "viewport") + self.mechanical("export", write=True, format="f3d", outputPath=path) + result = self.native("checkpoint", documentId=self.document_id, path=path) + self.check(self.fixture + " archive preserves native witness", equivalent(result["before"], result["after"]), result["archive"]) + for row in (result["source"], result["reopened"]): + self.bind_document(row) + self.save_artifact(self.native("file", path=path), "native-archive") + # Imported object is exact, registered and checkpointed before closing. + self.native("close", documentId=result["reopened"]["documentId"]) + self.native("activate", documentId=self.document_id) + + def cleanup(self): + if self.owned is None: + self.report["cleanup"] = "NOT_NEEDED" + return + try: + if self.ambiguous: + raise RuntimeError("Unknown operation still outstanding; owned documents retained") + inventory = self.native("inventory") + if inventory.get("provisionalImportCount"): + raise RuntimeError("An imported native object lacks public identity; retained in native registry") + for row in inventory["documents"]: + self.bind_document(row) + # If fixture assertions failed, checkpoint only the exact currently + # active owned object. Other uncheckpointed docs remain retained. + if self.document_id in self.owned.manifest["documents"]: + record = self.owned.manifest["documents"][self.document_id] + if not (record.get("checkpoint") or {}).get("verified"): + self.checkpoint(capture=False) + results = self.owned.cleanup() + self.report["cleanupEvidence"] = results + latest = self.owned.manifest.get("cleanupLatest", {}) + self.report["cleanupLatest"] = latest + if self.owned.manifest.get("pendingRegistrations"): + raise RuntimeError("An owned creation was not reconciled into native registry") + if any(x["status"] != "PASS" for x in (latest.values() if latest else results)): + raise RuntimeError("Exact guarded cleanup retained one or more documents") + restored = self.native("restore") + prefs = self.call("fusion_get_preferences")["current"] + _, _, current = self.runtime_observation(require_versions=True) + self.check("Original documents restored", restored["documents"] == self.report["baseline"]["native"]["documents"], restored) + self.check("Preferences preserved", prefs == self.report["baseline"]["preferences"], prefs) + original = self.report["baseline"]["runtime"] + stable_fields = set(original) - {"runningVersion"} + self.check("Runtime configuration and development pin preserved", all(current.get(k) == original[k] for k in stable_fields), {k: current.get(k) for k in original}) + live_before = self.report["baseline"]["liveIdentity"] + live_after = current["liveIdentity"] + self.check("Actual native runtime identity preserved", all(live_after.get(k) == live_before.get(k) for k in + ("host", "pid", "port", "executablePath", "processCreatedUtc")), live_after) + self.report["finalRuntimeIdentity"] = live_after + self.report["finalHost"] = restored + self.report["cleanup"] = "PASS" + except BaseException as exc: + self.report["cleanup"] = "FAIL" + self.report["cleanupError"] = str(exc) + self.persist() + + def run(self): + try: + baseline = self.baseline(require_versions=True) + self.report["baseline"] = baseline + capabilities = self.call("fusion_mech_capabilities") + self.session = capabilities["sessionId"] + self.report["sessionId"] = self.session + begin = self.native("begin", baseline=baseline["native"]["documents"]) + self.owned = OwnedRun(self.directory / "ownership.json", self.args.target, self.id, host_scratch=begin["scratch"]) + self.owned.manifest["sessionId"] = self.session + self.owned.manifest["host"] = self.args.host_name + self.owned.persist() + for iteration in range(1, self.args.iterations + 1): + for fixture in self.args.fixtures: + self.fixture = "%02d-%s" % (iteration, fixture) + start = len(self.report["checks"]) + getattr(self, fixture)() + if self.args.inject_failure == "after-model": + raise RuntimeError("Requested cleanup diagnostic after model creation") + self.checkpoint() + self.native("close", documentId=self.document_id) + self.report["fixtures"].append({"name": self.fixture, "status": "PASS", "checks": len(self.report["checks"]) - start}) + self.document_id = None + self.report["native"] = "PASS" + except BaseException as exc: + self.report["native"] = "FAIL" + self.report["failure"] = {"type": type(exc).__name__, "message": str(exc), "fixture": self.fixture} + finally: + self.cleanup() + completed = {row["name"] for row in self.report["fixtures"]} + for name in self.report["requestedFixtures"]: + if name not in completed: + self.report["fixtures"].append({"name": name, "status": "FAIL" if name == self.report.get("failure", {}).get("fixture") else "NOT_RUN"}) + self.report["overall"] = "VISUAL_PENDING" if self.report["native"] == "PASS" and self.report["cleanup"] == "PASS" else "FAIL" + self.report["counts"] = {status: sum(x["status"] == status for x in self.report["checks"]) for status in ("PASS", "FAIL")} + self.persist() + return 0 if self.report["overall"] == "VISUAL_PENDING" else 1 + + +def parser(): + command = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = command.add_subparsers(dest="mode", required=True) + sub.add_parser("plan", help="Print offline fixture/ownership contract; no host calls") + for mode in ("baseline", "run", "recover"): + p = sub.add_parser(mode) + p.add_argument("--target", required=True) + p.add_argument("--host-name", required=True, help="Expected native platform.node(), independent of routing alias") + p.add_argument("--evidence", required=True) + if mode == "run": + p.add_argument("--expect-bridge", required=True) + p.add_argument("--expect-addin", required=True) + p.add_argument("--iterations", type=int, choices=range(1, 6), default=1) + p.add_argument("--fixtures", nargs="+", choices=PLAN["fixtures"], default=["bracket", "assembly"]) + p.add_argument("--inject-failure", choices=("after-new", "after-model"), help="Owned-fixture cleanup diagnostic; expected nonzero run") + p.add_argument("--shotlog-channel", help="Inject every captured viewport into this explicitly selected channel") + p.add_argument("--supplier-file", help="Explicit existing host STEP/SAT path; never downloaded automatically") + p.add_argument("--supplier-sha256") + p.add_argument("--supplier-name") + p.add_argument("--supplier-part") + p.add_argument("--supplier-url") + review = sub.add_parser("review", help="Record independent visual annotations bound to exact PNG hashes") + review.add_argument("--evidence", required=True) + review.add_argument("--reviewer", required=True) + review.add_argument("--annotations", required=True) + return command + + +def recover(args, campaign_type=Campaign): + directory = Path(args.evidence).resolve() + report = json.loads((directory / "report.json").read_text()) + ownership = json.loads((directory / "ownership.json").read_text()) + if ownership["target"] != args.target or ownership.get("host") != args.host_name: + raise ValueError("Recovery target/host differs from recorded ownership") + args.shotlog_channel = None + runner = campaign_type(args) + runner.id, runner.report, runner.session = ownership["runId"], report, ownership["sessionId"] + runner.client = BridgeClient(args.target, runner.id, ai_thread="Fusion mechanical Ralph recovery", evidence_dir=directory / ("recovery-calls-" + uuid.uuid4().hex[:8])) + args.expect_bridge = report["baseline"]["liveIdentity"]["health"]["bridgeVersion"] + args.expect_addin = report["baseline"]["addin"]["version"] + runner.runtime_observation(require_versions=True) + caps = runner.call("fusion_mech_capabilities") + if caps["sessionId"] != runner.session: + raise RuntimeError("Runtime session changed; cannot prove native object identity. Documents retained.") + last = ownership.get("lastDispatch") + if last: + original = CallResult({"outcome": "unknown_after_interruption"}, observe({}), + [{"verb": "fusion_mechanical", "args": last, "requestJson": json.dumps(last), + "source": "persisted lastDispatch"}], False, 0, True) + reconciled = runner.client.reconcile_mechanical(original, runner.session, last["operationId"], polls=1) + write_json(directory / ("recovery-ledger-" + uuid.uuid4().hex[:8] + ".json"), reconciled) + if reconciled["state"] not in ("succeeded", "failed"): + raise RuntimeError("Last operation lacks a terminal status; no recovery mutation") + runner.owned = OwnedRun.resume(directory / "ownership.json", args.target, runner.id) + inventory = runner.native("inventory") + for row in inventory["documents"]: + runner.bind_document(row) + runner.owned.manifest.get("pendingRegistrations", {}).pop(row["documentId"], None) + runner.owned.persist() + # Recovery only closes already verified checkpoints. It never exports an + # unknown current model or guesses ownership from a document-list delta. + runner.cleanup() + report["overall"] = "VISUAL_PENDING" if report["native"] == "PASS" and report["cleanup"] == "PASS" else "FAIL" + report["counts"] = {status: sum(row["status"] == status for row in report["checks"]) + for status in ("PASS", "FAIL")} + runner.persist() + return 0 if report["cleanup"] == "PASS" else 1 + + +def main(argv=None): + args = parser().parse_args(argv) + if args.mode == "plan": + print(json.dumps(PLAN, indent=2)) + return 0 + if args.mode == "review": + path = Path(args.evidence) / "report.json" + report = json.loads(path.read_text()) + annotations = json.loads(Path(args.annotations).read_text()) + report["visual"] = visual_review(report, annotations, args.reviewer) + report["overall"] = "PASS" if report["native"] == "PASS" and report["cleanup"] == "PASS" and report["visual"]["status"] == "PASS" else "FAIL" + write_json(path, report) + print(json.dumps({"overall": report["overall"], "visual": report["visual"]}, indent=2)) + return 0 if report["overall"] == "PASS" else 1 + if args.mode == "recover": + return recover(args) + directory = Path(args.evidence) + if directory.exists(): + raise ValueError("Evidence directory must be new; use recover for an interrupted run") + if args.mode == "run" and "supplier" in args.fixtures: + if not all((args.supplier_file, args.supplier_sha256, args.supplier_name, args.supplier_part, args.supplier_url)): + raise ValueError("Supplier fixture requires explicit path, SHA-256, supplier, part and HTTPS source URL") + if len(args.supplier_sha256) != 64 or any(c not in "0123456789abcdefABCDEF" for c in args.supplier_sha256): + raise ValueError("Invalid supplier SHA-256") + if args.mode == "run" and len(set(args.fixtures)) != len(args.fixtures): + raise ValueError("Each fixture can appear once; use bounded --iterations for repetition") + directory.mkdir(parents=True) + runner = Campaign(args) + runner.persist() + if args.mode == "baseline": + runner.report.update(native="NOT_RUN", cleanup="NOT_NEEDED") + try: + runner.report["baseline"] = runner.baseline() + runner.report["overall"] = "BASELINE_RECORDED" + code = 0 + except BaseException as exc: + runner.report["overall"] = "BASELINE_REFUSED" + runner.report["failure"] = {"type": type(exc).__name__, "message": str(exc)} + code = 1 + runner.persist() + print(json.dumps({"overall": runner.report["overall"], "report": str(directory / "report.json")}, indent=2)) + return code + def interrupted(signum, _frame): + raise KeyboardInterrupt("Signal " + str(signum)) + signal.signal(signal.SIGTERM, interrupted) + signal.signal(signal.SIGINT, interrupted) + result = runner.run() + print(json.dumps({"overall": runner.report["overall"], "counts": runner.report.get("counts"), "report": str(directory / "report.json")}, indent=2)) + return result + + +if __name__ == "__main__": + try: + sys.exit(main()) + except (RuntimeError, ValueError, OSError) as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) diff --git a/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_support.py b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_support.py new file mode 100644 index 0000000..4bea870 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_support.py @@ -0,0 +1,423 @@ +"""Small, stdlib-only support for the background mechanical Ralph campaign. + +The native adapter uses mechanical.core only to obtain the public session-bound +document identity. Geometry measurements use independent Autodesk API calls. +No document is selected by name or creationId, and no preference is written. +""" +import hashlib +import json +import math +import base64 +from pathlib import Path, PureWindowsPath +import struct + + +def write_json(path, value): + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(value, indent=2, allow_nan=False) + "\n") + temporary.replace(path) + + +def equivalent(a, b, tolerance=1e-7): + if isinstance(a, bool) or isinstance(b, bool): + return type(a) is type(b) and a == b + if isinstance(a, (float, int)) and isinstance(b, (float, int)): + return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=tolerance, abs_tol=tolerance) + if isinstance(a, dict) and isinstance(b, dict): + return a.keys() == b.keys() and all(equivalent(a[k], b[k], tolerance) for k in a) + if isinstance(a, list) and isinstance(b, list): + return len(a) == len(b) and all(equivalent(x, y, tolerance) for x, y in zip(a, b)) + return a == b + + +def health_identity_request(runtime, host): + """Accept only a single discovered native process and a concrete TCP port.""" + pids = runtime.get("livePids") + port = runtime.get("spawn", {}).get("runtimePort") + if not isinstance(pids, list) or len(pids) != 1 or type(pids[0]) is not int or pids[0] <= 0: + raise ValueError("Live bridge inventory must identify exactly one process") + if type(port) is not int or not 1 <= port <= 65535 or runtime.get("port") != port: + raise ValueError("Discovered bridge runtime port is absent or inconsistent") + path = PureWindowsPath(runtime.get("entrypoint", "")) + expected_image = runtime.get("expectedImage") + if not path.is_absolute() or ".." in path.parts or not isinstance(expected_image, str) or path.name.lower() != expected_image.lower(): + raise ValueError("Discovered executable identity is incomplete") + if runtime.get("spawn", {}).get("kind") != "exe" or runtime.get("instanceCount") != 1: + raise ValueError("A single native executable bridge is required") + return {"host": host, "pid": pids[0], "port": port, "executablePath": str(path), "image": expected_image} + + +def health_probe_script(request): + """Read process/listener identity and GET only its discovered loopback health.""" + encoded = base64.b64encode(json.dumps(request).encode()).decode() + return r''' +$ErrorActionPreference = 'Stop' +try { + $ralphExpected = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('__REQUEST_B64__')) | ConvertFrom-Json + $ralphPort = [int]$ralphExpected.port + $ralphOwner = [int]$ralphExpected.pid + if ([Environment]::MachineName -ine [string]$ralphExpected.host) { throw 'Native host identity differs' } + $ralphProcess = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $ralphOwner) + if (-not $ralphProcess -or $ralphProcess.Name -ine [string]$ralphExpected.image) { throw 'Inventory PID/image is not the native bridge' } + if ([IO.Path]::GetFullPath($ralphProcess.ExecutablePath) -ine [IO.Path]::GetFullPath([string]$ralphExpected.executablePath)) { throw 'Native executable path differs from inventory' } + $ralphListeners = @(Get-NetTCPConnection -State Listen -LocalPort $ralphPort) + if ($ralphListeners.Count -eq 0) { throw 'Discovered port has no listener' } + foreach ($ralphListener in $ralphListeners) { + if ($ralphListener.OwningProcess -ne $ralphOwner -or $ralphListener.LocalAddress -notin @('127.0.0.1','::1')) { throw 'Listener is not owned by the expected loopback bridge' } + } + $ralphHealth = Invoke-RestMethod -Uri ('http://127.0.0.1:' + $ralphPort + '/health') -Method Get -TimeoutSec 8 + $ralphAgain = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $ralphOwner) + if (-not $ralphAgain -or $ralphAgain.CreationDate -ne $ralphProcess.CreationDate -or $ralphAgain.ExecutablePath -ine $ralphProcess.ExecutablePath) { throw 'Native process changed during health observation' } + $ralphListenersAfter = @(Get-NetTCPConnection -State Listen -LocalPort $ralphPort) + if ($ralphListenersAfter.Count -eq 0) { throw 'Listener disappeared during health observation' } + foreach ($ralphListener in $ralphListenersAfter) { + if ($ralphListener.OwningProcess -ne $ralphOwner -or $ralphListener.LocalAddress -notin @('127.0.0.1','::1')) { throw 'Listener changed during health observation' } + } + @{ host=[Environment]::MachineName; pid=$ralphOwner; port=$ralphPort; executablePath=$ralphProcess.ExecutablePath; + image=$ralphProcess.Name; processCreatedUtc=$ralphProcess.CreationDate.ToUniversalTime().ToString('o'); + listenerAddresses=@($ralphListenersAfter | ForEach-Object { $_.LocalAddress }); + stable=$true; health=$ralphHealth } | ConvertTo-Json -Depth 12 -Compress + exit 0 +} catch { + [Console]::Error.WriteLine($_.Exception.Message) + exit 1 +} +'''.replace('__REQUEST_B64__', encoded) + + +def validate_live_identity(request, evidence, bridge_version=None, addin_version=None): + """An inventory version is descriptive; live identity/version is decisive.""" + if not (evidence.get("host", "").lower() == request["host"].lower() and + evidence.get("pid") == request["pid"] and evidence.get("port") == request["port"] and + evidence.get("image", "").lower() == request["image"].lower() and + PureWindowsPath(evidence.get("executablePath", "")) == PureWindowsPath(request["executablePath"]) and + evidence.get("stable") is True and evidence.get("processCreatedUtc")): + raise ValueError("Observed native process identity differs from inventory or host") + addresses = evidence.get("listenerAddresses") + if not isinstance(addresses, list) or not addresses or any(x not in ("127.0.0.1", "::1") for x in addresses): + raise ValueError("Native health was not bound to an owned loopback listener") + health = evidence.get("health", {}) + if "sidecarPort" in health: + raise ValueError("Sidecar health cannot attest the native executable version") + if not (health.get("status") == "ok" and health.get("exePort") == request["port"] and + health.get("fusion", {}).get("running") is True and health.get("busy") is False and + health.get("addin", {}).get("busy") is False): + raise ValueError("Native loopback health is not idle and ready on the discovered port") + if bridge_version is not None and health.get("bridgeVersion") != bridge_version: + raise ValueError("Actual native bridge version differs from expected version") + if addin_version is not None and health.get("addin", {}).get("version") != addin_version: + raise ValueError("Actual native add-in version differs from expected version") + return evidence + + +def artifact(path, expected_sha=None): + path = Path(path) + data = path.read_bytes() + digest = hashlib.sha256(data).hexdigest() + if expected_sha is not None and digest != expected_sha: + raise ValueError("Artifact bytes differ from the recorded SHA-256: " + str(path)) + result = {"path": str(path.resolve()), "sha256": digest, "bytes": len(data)} + if path.suffix.lower() == ".png": + if data[:8] != b"\x89PNG\r\n\x1a\n" or data[12:16] != b"IHDR" or len(data) < 24: + raise ValueError("Invalid PNG header") + width, height = struct.unpack(">II", data[16:24]) + if width < 640 or height < 480: + raise ValueError("Viewport capture is too small for review") + result.update(width=width, height=height) + return result + + +def visual_review(report, annotations, reviewer): + """Bind explicit human/independent-agent review to every captured byte set.""" + if not reviewer.strip(): + raise ValueError("An identified reviewer is required") + images = [x for x in report.get("artifacts", []) if x.get("kind") == "viewport"] + if not images: + raise ValueError("No viewport evidence exists") + by_sha = {row.get("sha256"): row for row in annotations} + if len(by_sha) != len(annotations) or set(by_sha) != {x["sha256"] for x in images}: + raise ValueError("Review must identify every current viewport SHA exactly once") + for image in images: + artifact(image["path"], image["sha256"]) + row = by_sha[image["sha256"]] + if row.get("verdict") not in ("PASS", "FAIL") or not isinstance(row.get("notes"), str) or not row["notes"].strip(): + raise ValueError("Each image needs a PASS/FAIL verdict and concrete visual notes") + return {"status": "PASS" if all(x["verdict"] == "PASS" for x in annotations) else "FAIL", + "reviewer": reviewer, "annotations": annotations, + "scope": "Native model viewport appearance; does not prove application chrome or picker UI."} + + +BASELINE_SCRIPT = r''' +import platform, tempfile +result = {'host': platform.node(), 'platform': platform.system(), 'fusionVersion': app.version, + 'temporaryDirectory': tempfile.gettempdir(), + 'activeCreationId': app.activeDocument.creationId if app.activeDocument else None, + 'activeProductType': app.activeProduct.productType if app.activeProduct else None, + 'activeCommand': ui.activeCommand, + 'documents': [{'creationId': d.creationId, 'name': d.name, + 'modified': d.isModified, 'saved': d.isSaved} for d in app.documents]} +''' + + +# This program executes inside Fusion through the public modeling-script verb. +# All actions are bounded and use a session-local registry of actual API objects. +# Import records the returned object before any readback that could fail. A +# timed-out adapter is reconciled by inventory, never re-dispatched blindly. +NATIVE_PROGRAM = r''' +import base64, builtins, csv, hashlib, json, math, os, platform, re, sys, tempfile + +def require(condition, message): + if not condition: + raise RuntimeError(message) + +def same(a, b): + if isinstance(a, bool) or isinstance(b, bool): + return type(a) is type(b) and a == b + if isinstance(a, (int, float)) and isinstance(b, (int, float)): + return math.isfinite(a) and math.isfinite(b) and math.isclose(a, b, rel_tol=1e-7, abs_tol=1e-7) + if isinstance(a, dict) and isinstance(b, dict): + return a.keys() == b.keys() and all(same(a[k], b[k]) for k in a) + if isinstance(a, list) and isinstance(b, list): + return len(a) == len(b) and all(same(x, y) for x, y in zip(a, b)) + return a == b + +def vector(p): + return [p.x * 10, p.y * 10, p.z * 10] + +def body_measure(body): + properties = body.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy) + box = body.boundingBox + return {'name': body.name, 'volume': properties.volume * 1000, 'mass': properties.mass, + 'centerOfMass': vector(properties.centerOfMass), 'faces': body.faces.count, + 'bounds': [vector(box.minPoint), vector(box.maxPoint)], + 'material': body.material.name if body.material else None} + +def witness(document): + design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType')) + require(design is not None, 'Owned document is no longer a design') + root = design.rootComponent + components, occurrences = [], [] + all_bodies = [body_measure(b) for b in root.bRepBodies] + for comp in design.allComponents: + components.append({'name': '' if comp == root else comp.name, + 'partNumber': comp.partNumber, 'description': comp.description, + 'material': comp.material.name if comp.material else None, + 'attributes': {a.name: a.value for a in comp.attributes if a.groupName == 'AdomMechanical'}, + 'bodies': [body_measure(b) for b in comp.bRepBodies], + 'sketches': [{'name': s.name, 'fullyConstrained': s.isFullyConstrained, + 'curves': s.sketchCurves.count, 'profiles': s.profiles.count} for s in comp.sketches]}) + for occurrence in root.allOccurrences: + matrix = list(occurrence.transform2.asArray()) + for index in (3, 7, 11): matrix[index] *= 10 + bodies = [body_measure(b) for b in occurrence.bRepBodies] + all_bodies.extend(bodies) + occurrences.append({'path': occurrence.fullPathName, 'component': occurrence.component.name, + 'transform': matrix, 'bodies': bodies}) + props = root.getPhysicalProperties(adsk.fusion.CalculationAccuracy.HighCalculationAccuracy) + return {'parameters': {p.name: p.expression for p in design.allParameters}, + 'timelineCount': design.timeline.count, + 'components': sorted(components, key=lambda c: c['name']), + 'occurrences': sorted(occurrences, key=lambda o: o['path']), + 'physical': {'volume': props.volume * 1000, 'mass': props.mass, + 'centerOfMass': vector(props.centerOfMass)}, + 'instanceVolume': sum(b['volume'] for b in all_bodies), + 'instanceCount': len(all_bodies)} + +require(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host') +require(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID') +modules = [m for n, m in list(sys.modules.items()) + if n.endswith('.mechanical.core') and getattr(m, 'SESSION_ID', None) == REQUEST['sessionId']] +require(len(modules) == 1, 'Exact serving mechanical session is unavailable; no ownership inference') +core = modules[0] +registries = getattr(builtins, '_adom_mechanical_ralph_runs', None) +if registries is None: + registries = {} + builtins._adom_mechanical_ralph_runs = registries +action = REQUEST['action'] +run = registries.get(REQUEST['runId']) + +def documents(): + return [{'creationId': d.creationId, 'name': d.name, 'modified': d.isModified, 'saved': d.isSaved} + for d in app.documents] + +def idle(): + require(ui.activeCommand in ('', 'SelectCommand', 'Select'), 'Interactive command active; refusing document lifecycle action') + +def fingerprint(document): + return {'creationId': document.creationId, 'name': document.name, + 'modified': document.isModified, 'saved': document.isSaved} + +def owned(document_id, allow_closed=False): + require(document_id in run['documents'], 'Document has no exact native ownership entry') + row = run['documents'][document_id] + if row.get('closed'): + require(allow_closed, 'Owned document is already closed') + return row + document = row['object'] + require(any(d == document for d in app.documents), 'Exact owned document is no longer open') + require(not any(d == document for d in run['baselineObjects']), 'Refusing a baseline document') + require(document.name == row['name'] and not document.isSaved, 'Owned document was renamed or saved externally') + design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType')) + marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner') + require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed') + marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId') + require(marker and marker.value == document_id, 'Runtime document marker changed') + return row + +def path_in_scratch(path): + root = os.path.realpath(run['scratch']) + path = os.path.abspath(path) + require(os.path.commonpath([root, os.path.realpath(path)]) == root and os.path.realpath(path) != root, + 'Artifact is not inside the owned scratch directory') + require(not os.path.islink(path), 'Artifact is a symlink') + return path + +def file_info(path): + path = path_in_scratch(path) + require(os.path.isfile(path), 'Checkpoint file is absent') + require(os.path.getsize(path) <= 32 * 1024 * 1024, 'Fixture artifact exceeds 32 MiB budget') + with open(path, 'rb') as stream: data = stream.read() + return {'path': path.replace('\\', '/'), 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data)} + +def register(document, document_id, role): + require(not any(d == document for d in run['baselineObjects']), 'Creation returned a baseline document') + require(document_id not in run['documents'], 'Runtime ID is already registered') + # Retain exact object first, including if marker creation later fails. + row = {'object': document, 'documentId': document_id, 'role': role, + 'creationId': document.creationId, 'name': document.name, 'closed': False, 'checkpoint': None} + run['documents'][document_id] = row + require(not document.isSaved and document.name.startswith('Untitled'), 'Only new unnamed designs are owned') + design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType')) + design.rootComponent.attributes.add('AdomRalph', 'owner', REQUEST['runId']) + design.rootComponent.attributes.add('AdomRalph', 'documentId', document_id) + return row + +def public_row(row): + return {k: v for k, v in row.items() if k not in ('object', 'witness')} + +if action == 'begin': + idle() + require(run is None, 'Native run registry already exists; use recover') + require(app.documents.count > 0, 'An existing active design is required for a read-only baseline') + baseline = documents() + require(baseline == REQUEST['baseline'], 'Documents changed after baseline; refusing to start') + scratch = os.path.join(tempfile.gettempdir(), 'adom-fusion-ralph', REQUEST['runId']) + require(not os.path.exists(scratch), 'Run scratch already exists') + parent = os.path.dirname(scratch) + require(os.path.realpath(parent) == os.path.abspath(parent), 'Scratch parent resolves through a link') + os.makedirs(scratch) + run = {'sessionId': REQUEST['sessionId'], 'documents': {}, 'scratch': scratch, + 'baselineObjects': list(app.documents), 'baseline': baseline, + 'activeObject': app.activeDocument, 'activeFingerprint': fingerprint(app.activeDocument)} + registries[REQUEST['runId']] = run + result = {'success': True, 'scratch': scratch.replace('\\', '/'), 'documents': baseline} +else: + require(run is not None and run['sessionId'] == REQUEST['sessionId'], 'Native registry/session missing; no close attempted') + if action == 'inventory': + result = {'success': True, 'documents': [public_row(r) for r in run['documents'].values()], + 'provisionalImportCount': len(run.get('provisionalImports', [])), + 'scratch': run['scratch'].replace('\\', '/')} + elif action == 'register': + idle() + document, design, document_id = core.active(app) + require(document_id == REQUEST['documentId'], 'Active runtime document differs from typed creation result') + row = register(document, document_id, REQUEST['role']) + result = {'success': True, **public_row(row)} + elif action in ('witness', 'verify', 'activate', 'close', 'checkpoint', 'capture'): + row = owned(REQUEST['documentId'], action in ('close', 'verify')) + document = row['object'] + if action == 'verify': + if not row.get('closed'): + checkpoint = row['checkpoint'] + require(checkpoint is not None, 'No verified native checkpoint') + require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed') + require(same(witness(document), row['witness']), 'Model changed since checkpoint') + result = {'success': True, 'documentId': row['documentId'], 'ownerRunId': REQUEST['runId'], 'closed': row['closed']} + elif action == 'witness': + require(app.activeDocument == document, 'Active document changed; no implicit activation') + result = {'success': True, 'documentId': row['documentId'], 'witness': witness(document)} + elif action == 'activate': + idle() + require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']), + 'User selected an external document; refusing activation') + if app.activeDocument != document: + require(document.activate(), 'Owned document activation failed') + result = {'success': True, 'documentId': row['documentId']} + elif action == 'close': + idle() + if not row['closed']: + require(any(app.activeDocument == r['object'] for r in run['documents'].values() if not r['closed']), + 'User selected an external document; refusing lifecycle changes') + checkpoint = row['checkpoint'] + require(checkpoint is not None, 'No verified native checkpoint') + require(file_info(checkpoint['path'])['sha256'] == checkpoint['sha256'], 'Checkpoint bytes changed') + require(same(witness(document), row['witness']), 'Model changed since checkpoint') + require(document.close(False), 'Owned document close returned false') + require(not any(d == document for d in app.documents), 'Owned document still appears in open documents') + row['closed'] = True + result = {'success': True, 'closedDocumentId': row['documentId'], 'closed': True} + elif action == 'capture': + require(app.activeDocument == document, 'Active document changed before capture') + path = path_in_scratch(REQUEST['path']) + require(not os.path.exists(path), 'Capture destination already exists') + viewport = app.activeViewport + viewport.fit() + viewport.refresh() + require(viewport.saveAsImageFile(path, 1280, 900), 'Viewport capture failed') + result = {'success': True, **file_info(path)} + else: + idle() + require(app.activeDocument == document, 'Active document changed before roundtrip') + path = path_in_scratch(REQUEST['path']) + archive = file_info(path) + before = witness(document) + options = app.importManager.createFusionArchiveImportOptions(path) + imported = app.importManager.importToNewDocument(options) + # Keep the actual returned API object before any fragile getter or + # identity lookup. A provisional object can only be retained/reported. + provisional = {'object': imported} + run.setdefault('provisionalImports', []).append(provisional) + require(imported is not None and app.activeDocument == imported, 'Archive import returned no active document') + reopened_doc, reopened_design, reopened_id = core.active(app) + require(reopened_doc == imported, 'Imported document identity mismatch') + reopened = register(imported, reopened_id, row['role'] + '-reopened') + run['provisionalImports'].remove(provisional) + after = witness(imported) + require(same(before, after), 'Native archive roundtrip changed measured model state') + # Mark both sides only after independent semantic comparison. + for item in (row, reopened): + item['checkpoint'] = archive + item['witness'] = before + result = {'success': True, 'archive': archive, 'before': before, 'after': after, + 'reopened': public_row(reopened), 'source': public_row(row)} + elif action == 'file': + info = file_info(REQUEST['path']) + with open(info['path'], 'rb') as stream: data = stream.read() + result = {'success': True, **info, 'base64': base64.b64encode(data).decode('ascii')} + if REQUEST.get('csv'): + with open(info['path'], encoding='utf-8', newline='') as stream: + result['rows'] = list(csv.DictReader(stream)) + elif action == 'restore': + idle() + require(not run.get('provisionalImports'), 'Unresolved imported object remains; restoration refused') + require(all(row['closed'] for row in run['documents'].values()), 'Owned documents remain open') + require(len(list(app.documents)) == len(run['baselineObjects']) and + all(any(d == original for d in app.documents) for original in run['baselineObjects']), + 'Document set changed; refusing to change active document') + require(documents() == run['baseline'], 'Original document flags changed') + original = run['activeObject'] + if app.activeDocument != original: + require(original.activate(), 'Original document activation failed') + require(fingerprint(original) == run['activeFingerprint'], 'Original active document changed') + result = {'success': True, 'documents': documents(), 'activeCreationId': original.creationId} + else: + raise RuntimeError('Unknown native action') +''' + + +def native_script(action, *, run_id, host, session_id, **values): + request = dict(values, action=action, runId=run_id, host=host, sessionId=session_id) + # repr around JSON prevents input being interpreted as Python source. + return "import json\nREQUEST = json.loads(" + repr(json.dumps(request)) + ")\n" + NATIVE_PROGRAM diff --git a/docs/mechanical-verification/cp9-readonly/evidence-manifest.json b/docs/mechanical-verification/cp9-readonly/evidence-manifest.json new file mode 100644 index 0000000..ee6016b --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/evidence-manifest.json @@ -0,0 +1,261 @@ +{ + "files": [ + { + "path": "accepted-diagnostic.json", + "bytes": 4938, + "sha256": "cf09823dad1bd1a1cbaf61b436b6fd9d5a1e77d08059386f4dc412575b5d3275" + }, + { + "path": "baseline.json", + "bytes": 12391, + "sha256": "bc9aa832de32e98835d73a1d06ec6ff498ef313cb6f271208a218536b841c441" + }, + { + "path": "calls/0001-fusion_state.json", + "bytes": 19212, + "sha256": "986a1a6481f001aa35c12eea319a61851f575a0962427b2d122ece0922c45fc6" + }, + { + "path": "calls/0002-fusion_addin_status.json", + "bytes": 2085, + "sha256": "f8fb2a02caec14107c0a52cd80a09b835af0d08afb02407371ebebd30aab4a73" + }, + { + "path": "calls/0003-bridge_list.json", + "bytes": 106328, + "sha256": "c0f4fe3351484f3019da1675994bdd69acdd4dc239a2783275306a4a36f9c2c5" + }, + { + "path": "calls/0004-run_script.json", + "bytes": 10962, + "sha256": "647ac42a6870ab2f5e0b6a95755023c231b92a2e88c939e878af401bc4a99246" + }, + { + "path": "calls/0005-fusion_document_info.json", + "bytes": 3848, + "sha256": "a1c06753d923b5660c09e2b11bdc9a27bdc2907fe24c2fa6e9121643862f31bb" + }, + { + "path": "calls/0006-fusion_run_modeling_script.json", + "bytes": 5495, + "sha256": "c2d3cb731b79c3d32b6c61087caa58aeaff62ae3acfe679ebf04ab58b711ee64" + }, + { + "path": "calls/0007-fusion_get_preferences.json", + "bytes": 4736, + "sha256": "084e842d85eaed160c19a99d78744cf5a8bf5194e2eca17e736625bab6a848b4" + }, + { + "path": "calls/0008-fusion_mech_capabilities.json", + "bytes": 628323, + "sha256": "9593b14e0040dc6242bd767a425eec7fa9833bbc039fc2e00d9ad93b6c7e51fa" + }, + { + "path": "calls/0009-fusion_run_modeling_script.json", + "bytes": 35563, + "sha256": "4317007b59f8cc1462b4cb8f035073e8b1f728943207abb24bb1cb7a13cd3b80" + }, + { + "path": "calls/0010-fusion_mechanical.json", + "bytes": 27067, + "sha256": "40c4ab6b513feac3f87ea463a92821250297f4dc1ce11ba88effc3c54a7175da" + }, + { + "path": "calls/0011-fusion_run_modeling_script.json", + "bytes": 38327, + "sha256": "8cc51b7aac6a282b7bd0c5a6fb8f42e119168ac2d732096562a44e5a69eaac7d" + }, + { + "path": "calls/0012-fusion_mechanical.json", + "bytes": 32579, + "sha256": "cb3ed4d83ca00018a73f967cfc22f29c003fb3d13afd06258ed10a436b404c60" + }, + { + "path": "calls/0013-fusion_run_modeling_script.json", + "bytes": 41585, + "sha256": "0134a008d61ee5abb705835383cdc21742d1dc3bf0bd445c12bf76962d12ffb9" + }, + { + "path": "calls/0014-fusion_mechanical.json", + "bytes": 32581, + "sha256": "ba7935dde2e8d04990a9076a039d13af57668d9fe540585232d8e1eb07c0a3ca" + }, + { + "path": "calls/0015-fusion_mechanical.json", + "bytes": 14772, + "sha256": "46ee93c413687e218125de5a7fc1166ca6b54d9ad0b90c15e12c7fa02d2668b0" + }, + { + "path": "calls/0016-fusion_run_modeling_script.json", + "bytes": 41585, + "sha256": "e364337ed8692dadc7fdc12c8152e4d4ff8b0d27c73910a89bb4f1d5c8296c8a" + }, + { + "path": "calls/0017-fusion_run_modeling_script.json", + "bytes": 77135, + "sha256": "417d1a4acd3ff0305a49d285819dbfe6823da2edee6720d63a9a924f42887230" + }, + { + "path": "calls/0018-fusion_run_modeling_script.json", + "bytes": 41585, + "sha256": "138a1c49d7c860640dca25077e1bdea4a16b26c33104354bb7e30559eaf223fa" + }, + { + "path": "calls/0019-fusion_mechanical.json", + "bytes": 32579, + "sha256": "1840a3fb0c05d68986178b3e0dfc58e1248b65931f044ff0f000dc13e48eb091" + }, + { + "path": "calls/0020-fusion_mechanical.json", + "bytes": 7264, + "sha256": "741044b4067e66ff037f5a69302fdec14a5c069451a3deb23175088b048dc37d" + }, + { + "path": "calls/0021-fusion_run_modeling_script.json", + "bytes": 50283, + "sha256": "f1e523b80668973f376aa37356ca6e3864f16b8daa5d68e7ff6aa2c92ba67e76" + }, + { + "path": "calls/0022-fusion_run_modeling_script.json", + "bytes": 212254, + "sha256": "4d8e59ff8e599918f203d8a3e632cf4373190a211f293a19d0295169313ce8b6" + }, + { + "path": "calls/0023-fusion_run_modeling_script.json", + "bytes": 38605, + "sha256": "1e90e773993d2b8f5f277ed18887b0e3c10303d9c71373c6a1d2edd69607db8f" + }, + { + "path": "calls/0024-fusion_run_modeling_script.json", + "bytes": 37873, + "sha256": "898ebba5c9f0a2ad3a66d4c85da539b2795ee65c28fa8b7c7bf44fa6ef2da5e8" + }, + { + "path": "calls/0025-fusion_run_modeling_script.json", + "bytes": 36439, + "sha256": "d16b321e3b06003f24e6d0e17ef37cc43e656c55163b38d414de2ee1d5ffc16f" + }, + { + "path": "calls/0026-fusion_run_modeling_script.json", + "bytes": 37503, + "sha256": "aba5c60cf489b199cc3f5e358a6ed96bb1c0939326b797d7e28c6883cfc2cbea" + }, + { + "path": "calls/0027-fusion_run_modeling_script.json", + "bytes": 35177, + "sha256": "b14c5950ac2bb8d4e630a7a92f57e3f4d70aceff05fd751a1105a4f8106a8f0a" + }, + { + "path": "calls/0028-fusion_run_modeling_script.json", + "bytes": 35034, + "sha256": "c6addd0768dee0ad5e9f5d33133505932c151d942f7993be1aba552684c96e65" + }, + { + "path": "calls/0029-fusion_run_modeling_script.json", + "bytes": 35177, + "sha256": "a515524528a260d89e11881e19266339c39d26f51e8e7127e259da4e3ce15c86" + }, + { + "path": "calls/0030-fusion_run_modeling_script.json", + "bytes": 35034, + "sha256": "5790c98cc6c60443d3b1976e879b41e01291ed8f181b2e190b2746bd2b4d353a" + }, + { + "path": "calls/0031-fusion_run_modeling_script.json", + "bytes": 35151, + "sha256": "b63f0b157b622bd249ffc260d7cd22255337a0985ea1463936d5ec2e89318850" + }, + { + "path": "calls/0032-fusion_get_preferences.json", + "bytes": 4742, + "sha256": "f3fb6e5e8dd25ba837bbb437387e66d3d5952be67fcdcdc0abb200616965c5c0" + }, + { + "path": "calls/0033-fusion_state.json", + "bytes": 19211, + "sha256": "a417f0abd3d6f598086c5f4e972463943a5e53e196398e89d753b2469b554825" + }, + { + "path": "calls/0034-fusion_addin_status.json", + "bytes": 2085, + "sha256": "910b1ec970426ae8efe69e61df9a4fc93a685ff25f70dfb5b0eb2e1c4eb2ab9a" + }, + { + "path": "calls/0035-bridge_list.json", + "bytes": 106329, + "sha256": "acc8e02a5a35e51a9f9072c89765c3642e48a630025e5b1caab0f738d738d9c6" + }, + { + "path": "calls/0036-run_script.json", + "bytes": 10962, + "sha256": "34222863215c519c229c76b5212e3f28ed396111dcf4e898d66a3fc17d8d2e31" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/diagnostic_scene.py", + "bytes": 11282, + "sha256": "bad536a1d0159eb5e2426e5e671510bc3e0178aee80866f477152f43c3b02073" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/fusion_harness.py", + "bytes": 28903, + "sha256": "0ded009f2ad0b5d8919ac215b9366df2b44d9ef98416e5f4a076e36656f88ec2" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/READONLY-PROBE.md", + "bytes": 6208, + "sha256": "01f33dde1a9bfa821b308a91d0889cadce7c6098af73cdcfbe98e66bc527ff41" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/read_scene_probe.py", + "bytes": 18532, + "sha256": "bc2bcf78675f28151faa4ab0c67276e553792cc0c6c206fad98718219e02814c" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/test_read_scene_probe.py", + "bytes": 15227, + "sha256": "8df35f06355717a0c8bfd64b66a951a4a349b9257aff0b14ff1db7a02edb8ba4" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_jobs.py", + "bytes": 17361, + "sha256": "4322d735104fb82f722fe03a1a806f6cc64fb264541ef22968111df14150a56f" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_mechanical.py", + "bytes": 39936, + "sha256": "ab7451dbc3dda32442128a6262aebf3b9b53884571c6174e9bc2151e5f912d2e" + }, + { + "path": "diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_support.py", + "bytes": 25311, + "sha256": "a152fed6766ba0e86f89a84b52ce9090bcabcbc3bf888ae1b7f7b08836895e27" + }, + { + "path": "independent-review.json", + "bytes": 882, + "sha256": "70520b5cccde5aecbb0106ff8027b5754d004bc766178536c707d242526db58c" + }, + { + "path": "live-runtime-identity.json", + "bytes": 1656, + "sha256": "df509d6f54d2b30846176c1b81a97c5090b4dd491427d326f1001f62089cc056" + }, + { + "path": "native-scene-probe.py", + "bytes": 18172, + "sha256": "ce82c574608f4f99ce442ced5906b653eff7f1d8314c7851a6a795271a13e397" + }, + { + "path": "ownership.json", + "bytes": 4008, + "sha256": "6402c38eeaa020aa7ed681f028260bba5de7315876fbed75a5dbd03334af4a20" + }, + { + "path": "report.json", + "bytes": 47102, + "sha256": "3324ae9e6fc6ad42d205c425aa2b6a832b414b27893d4dde9cdec3ab1d1bae54" + } + ], + "newImagesIncluded": 0, + "scope": "Structured evidence only; original failures preserved. Native images remain local." +} diff --git a/docs/mechanical-verification/cp9-readonly/independent-review.json b/docs/mechanical-verification/cp9-readonly/independent-review.json new file mode 100644 index 0000000..112c138 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/independent-review.json @@ -0,0 +1,28 @@ +{ + "status": "PASS_DIAGNOSTIC", + "qualification": "PARTIAL", + "reportSha256": "3324ae9e6fc6ad42d205c425aa2b6a832b414b27893d4dde9cdec3ab1d1bae54", + "verifiedCallCount": 36, + "passedChecks": 12, + "diagnosticSourceHashesVerified": 8, + "singleExactProbe": true, + "witnessUnchanged": true, + "archiveSha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "ownedDocumentsClosed": 2, + "originalRestored": [ + { + "creationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "modified": false, + "name": "Untitled", + "saved": false + } + ], + "bridgeProcessPid": 42780, + "renderStarted": false, + "imageArtifacts": 0, + "hostCallsByReviewer": 0, + "limitations": [ + "Camera object unavailable; no causal claim about why it was unavailable.", + "No rendering, render-future, full scene, global queue or visual acceptance proved." + ] +} diff --git a/docs/mechanical-verification/cp9-readonly/live-runtime-identity.json b/docs/mechanical-verification/cp9-readonly/live-runtime-identity.json new file mode 100644 index 0000000..94b3fb6 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/live-runtime-identity.json @@ -0,0 +1,51 @@ +{ + "reportedRunningVersion": "1.12.0-dev.5", + "installedVersion": "1.12.0-dev.8", + "expectedIdentity": { + "host": "ADOM_DREW2", + "pid": 42780, + "port": 50618, + "executablePath": "C:\\Users\\drew\\AppData\\Local\\Adom Bridge\\bridges-cache\\fusion360\\adom-fusion-bridge.exe", + "image": "adom-fusion-bridge.exe" + }, + "observed": { + "image": "adom-fusion-bridge.exe", + "listenerAddresses": [ + "127.0.0.1" + ], + "executablePath": "C:\\Users\\drew\\AppData\\Local\\Adom Bridge\\bridges-cache\\fusion360\\adom-fusion-bridge.exe", + "stable": true, + "processCreatedUtc": "2026-10-05T21:01:31.6721740Z", + "host": "ADOM_DREW2", + "health": { + "status": "ok", + "busy": false, + "longOp": null, + "led": "green", + "summary": "Fusion ready", + "tooltip": "Fusion 360 running, AdomBridge add-in connected. All fusion_* verbs available.", + "bridgeVersion": "1.12.0-dev.8", + "fusion": { + "installed": true, + "exe_path": "C:\\Users\\drew\\AppData\\Local\\Autodesk\\webdeploy\\production\\6a0c9611291d45bb9226980209917c3d\\FusionLauncher.exe", + "addins_dir": "C:\\Users\\drew\\AppData\\Roaming\\Autodesk\\FusionAddins", + "addin_installed": true, + "running": true + }, + "addin": { + "busy": false, + "version": "1.12.0-dev.8", + "pyThreads": 4 + }, + "shell": "rust", + "needsSignin": false, + "licensingDialog": false, + "windowTitle": "Untitled - Autodesk Fusion ", + "windowFactsDegraded": false, + "windowFactsError": null, + "exePort": 50618 + }, + "pid": 42780, + "port": 50618 + } +} diff --git a/docs/mechanical-verification/cp9-readonly/native-scene-probe.py b/docs/mechanical-verification/cp9-readonly/native-scene-probe.py new file mode 100644 index 0000000..c7c06cc --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/native-scene-probe.py @@ -0,0 +1,263 @@ +import threading +if threading.current_thread() is not threading.main_thread(): + raise RuntimeError('Native render reads require the main thread') +import builtins, json, platform, re, sys +REQUEST = json.loads('{"runId": "20261005T213050Z-422781656d64490da02934d1757322ed", "host": "ADOM_DREW2", "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a"}') +def require(condition, message): + if not condition: + raise RuntimeError(message) +def owned(document_id, allow_closed=False): + require(document_id in run['documents'], 'Document has no exact native ownership entry') + row = run['documents'][document_id] + if row.get('closed'): + require(allow_closed, 'Owned document is already closed') + return row + document = row['object'] + require(any((d == document for d in app.documents)), 'Exact owned document is no longer open') + require(not any((d == document for d in run['baselineObjects'])), 'Refusing a baseline document') + require(document.name == row['name'] and (not document.isSaved), 'Owned document was renamed or saved externally') + design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType')) + marker = design.rootComponent.attributes.itemByName('AdomRalph', 'owner') + require(marker and marker.value == REQUEST['runId'], 'Run ownership marker changed') + marker = design.rootComponent.attributes.itemByName('AdomRalph', 'documentId') + require(marker and marker.value == document_id, 'Runtime document marker changed') + return row +"""Bounded CP9 native read-only probe; import alone makes no native calls. + +Use owned_script with the maintained native registry. Availability is not render +success, native queue idleness, a complete scene witness or settings ownership. +""" +import inspect +import ast +import json +import math +from pathlib import Path +import threading + +MAX_ENVIRONMENTS = 32 +MAX_OUTPUT_BYTES = 128 * 1024 + + +def render_capability_probe(app, document, design, *, max_environments=16): + import adsk.core + import adsk.fusion + + def need(value, message): + if not value: raise RuntimeError(message) + + need(threading.current_thread() is threading.main_thread(), 'Native render reads require the main thread') + need(type(max_environments) is int and 0 <= max_environments <= MAX_ENVIRONMENTS, 'Invalid environment probe budget') + need(document is not None and document.isValid and design is not None and design.isValid, 'Owned native design is invalid') + need(app.activeDocument == document and design.parentDocument == document, 'Current native document/design identity differs') + document_count = app.documents.count + need(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget or has invalid count') + need(sum(app.documents.item(i) == document for i in range(document_count)) == 1, 'Exact owned document is not uniquely open') + result = {'probeVersion':1, 'status':'DIAGNOSTIC_ONLY', 'renderStarted':False, 'settingsWritten':False, + 'sceneWitnessComplete':False, 'nativeQueueIdle':'UNVERIFIED', 'exclusiveSettingsOwnership':'UNVERIFIED', + 'errors':[], 'classes':{}, 'camera':{}, 'rendering':{}, 'scene':{}, 'environments':{}, + 'gaps':['No render execution or RenderFuture instance was created.', + 'No global renderer queue/coexistence proof.', + 'Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.', + 'Environment IDs and names do not prove custom environment content identity.', + 'No admitted revision-to-pixels or full source snapshot claim.']} + + def text(value): + need(isinstance(value,str) and len(value) <= 256, 'Native string exceeds probe budget or has wrong type') + return value + + def number(value): + need(type(value) in (int,float) and math.isfinite(value), 'Native scalar is not a finite number') + return value + + def boolean(value): + need(type(value) is bool, 'Native scalar is not Boolean') + return value + + def enum(value): + need(not isinstance(value,(bool,str)), 'Native enum has invalid type') + result = int(value) + need(result == value, 'Native enum is not an exact integer') + return result + + def point(value, scale=10): + return [number(number(getattr(value,axis)) * scale) for axis in ('x','y','z')] + + def read(path, callback, unit=None, native_unit=None): + try: + data = callback() + row = {'available':True,'value':data} + if unit is not None: row['unit'] = unit + if native_unit is not None: row['nativeUnit'] = native_unit + return row + except Exception as error: + reason = str(error)[:512] + result['errors'].append({'field':path,'error':reason}) + return {'available':False,'error':reason} + + def skipped(reason): return {'available':None,'notRead':reason} + + def object_read(path, callback): + try: + value = callback() + need(value is not None and value.isValid, 'Native object absent or invalid') + text(value.objectType) + return value, {'available':True,'objectType':value.objectType} + except Exception as error: + reason = str(error)[:512] + result['errors'].append({'field':path,'error':reason}) + return None, {'available':False,'error':reason} + + def fields(destination, prefix, owner, specs): + for name, converter, unit, native_unit in specs: + destination[name] = read(prefix + '.' + name, lambda name=name,converter=converter:converter(getattr(owner,name)), unit, native_unit) + + result['documentBefore'] = {'modified':read('document.modified',lambda:boolean(document.isModified)), + 'saved':read('document.saved',lambda:boolean(document.isSaved))} + # Static class inspection does not invoke descriptors, constructors, start, + # activation, cancel or any native future polling. + class_members = {'RenderManager':('rendering','sceneSettings','renderEnvironments','lights','isRenderWorkspaceActive'), + 'Rendering':('startLocalRender','aspectRatio','resolution','resolutionWidth','resolutionHeight','renderQuality','isBackgroundTransparent'), + 'RenderFuture':('filename','imageWidth','imageHeight','progress','renderState','isValid'), + 'SceneSettings':('backgroundType','backgroundEnvironment','backgroundSolidColor','isDepthOfFieldEnabled'), + 'RenderEnvironment':('id','name','isCustomEnvironment')} + for name,members in class_members.items(): + try: + cls = getattr(adsk.fusion,name) + availability = {} + for member in members: + try: inspect.getattr_static(cls,member); availability[member] = True + except AttributeError: availability[member] = False + result['classes'][name] = {'available':True,'members':availability,'methodInvocation':False} + except Exception as error: + result['classes'][name] = {'available':False,'error':str(error)[:512]} + + camera_enums = {name:read('CameraTypes.'+name,lambda name=name:enum(getattr(adsk.core.CameraTypes,name))) + for name in ('OrthographicCameraType','PerspectiveCameraType','PerspectiveWithOrthoFacesCameraType')} + background_enums = {name:read('RenderSceneBackgroundTypes.'+name, + lambda name=name:enum(getattr(adsk.fusion.RenderSceneBackgroundTypes,name))) + for name in ('SolidColorRenderSceneBackgroundType','EnvironmentRenderSceneBackgroundType')} + result['enums'] = {'camera':camera_enums,'background':background_enums} + + viewport, result['viewport'] = object_read('activeViewport',lambda:app.activeViewport) + camera = None + if viewport is not None: + camera, result['camera']['object'] = object_read('camera',lambda:viewport.camera) + if camera is not None: + fields(result['camera'],'camera',camera,[('cameraType',enum,'native CameraTypes enum',None), + ('eye',point,'mm','cm'),('target',point,'mm','cm'),('upVector',lambda p:point(p,1),'unitless',None), + ('isFitView',boolean,None,None),('isSmoothTransition',boolean,None,None)]) + result['camera']['space'] = 'world/root' + kind = result['camera']['cameraType'].get('value') + if kind is not None and kind == camera_enums['OrthographicCameraType'].get('value'): + def extents(): + okay,width,height = camera.getExtents() + need(okay is True, 'Orthographic camera extent read refused') + return [number(number(width)*10),number(number(height)*10)] + result['camera']['extents'] = read('camera.getExtents',extents,'mm','cm') + result['camera']['perspectiveAngle'] = skipped('Orthographic camera') + elif kind is not None and kind in (camera_enums['PerspectiveCameraType'].get('value'),camera_enums['PerspectiveWithOrthoFacesCameraType'].get('value')): + result['camera']['perspectiveAngle'] = read('camera.perspectiveAngle',lambda:number(camera.perspectiveAngle), + 'native angular scalar; convention requires native qualification') + result['camera']['extents'] = skipped('Perspective camera') + else: + result['errors'].append({'field':'camera.cameraType','error':'Unknown/unreadable camera mode; conditional lens getters not read'}) + result['camera']['extents'] = result['camera']['perspectiveAngle'] = skipped('Unknown camera mode') + + manager, result['renderManager'] = object_read('design.renderManager',lambda:design.renderManager) + if manager is not None: + matched = read('renderManager.parentDesign',lambda:boolean(manager.parentDesign == design)) + result['renderManager']['ownerMatches'] = matched + if matched.get('value') is not True: + result['errors'].append({'field':'renderManager.parentDesign','error':'Foreign/unreadable renderer owner; remaining renderer objects not read'}) + manager = None + if manager is not None: + result['renderManager']['workspaceActive'] = read('renderManager.isRenderWorkspaceActive',lambda:boolean(manager.isRenderWorkspaceActive)) + rendering, result['rendering']['object'] = object_read('renderManager.rendering',lambda:manager.rendering) + if rendering is not None: + fields(result['rendering'],'rendering',rendering,[('aspectRatio',enum,'native RenderAspectRatios enum',None), + ('resolution',enum,'native RenderResolutions enum',None),('resolutionWidth',number,'px',None), + ('resolutionHeight',number,'px',None),('renderQuality',number,'native quality (25..100)',None), + ('isBackgroundTransparent',boolean,None,None)]) + scene, result['scene']['object'] = object_read('renderManager.sceneSettings',lambda:manager.sceneSettings) + if scene is not None: + specs = [('aspectRatio',enum,'native RenderAspectRatios enum',None),('aspectRatioWidth',number,'ratio',None), + ('aspectRatioHeight',number,'ratio',None),('backgroundType',enum,'native RenderSceneBackgroundTypes enum',None), + ('brightness',number,'lux',None),('cameraExposure',number,'EV',None),('cameraFocalLength',number,'mm','mm'), + ('cameraType',enum,'native CameraTypes enum',None),('groundOffset',lambda n:number(number(n)*10),'mm','cm'), + ('lightAngle',lambda n:number(math.degrees(number(n))),'deg','rad')] + specs += [(name,boolean,None,None) for name in ('isDepthOfFieldEnabled','isGroundDisplayed','isGroundFlattened','isGroundReflections')] + fields(result['scene'],'scene',scene,specs) + for condition,names in (('isDepthOfFieldEnabled',('centerOfFocus','depthOfFieldBlur')), + ('isGroundFlattened',('groundPosition',)),('isGroundReflections',('groundRoughness',))): + enabled = result['scene'][condition].get('value') + for name in names: + if enabled is True: + vector = name in ('centerOfFocus','groundPosition') + fields(result['scene'],'scene',scene,[(name,point if vector else number,'mm' if vector else 'unitless','cm' if vector else None)]) + else: result['scene'][name] = skipped(condition + ' is false or unreadable') + background = result['scene']['backgroundType'].get('value') + if background is not None and background == background_enums['SolidColorRenderSceneBackgroundType'].get('value'): + def color(): + value = scene.backgroundSolidColor + return {name:number(getattr(value,name)) for name in ('red','green','blue','opacity')} + result['scene']['backgroundSolidColor'] = read('scene.backgroundSolidColor',color,'RGBA 0..255; opacity ignored by scene') + result['scene']['backgroundEnvironment'] = skipped('Solid background') + elif background is not None and background == background_enums['EnvironmentRenderSceneBackgroundType'].get('value'): + env, descriptor = object_read('scene.backgroundEnvironment',lambda:scene.backgroundEnvironment) + if env is not None: + fields(descriptor,'scene.backgroundEnvironment',env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)]) + result['scene']['backgroundEnvironment'] = descriptor + result['scene']['backgroundSolidColor'] = skipped('Environment background') + else: + result['errors'].append({'field':'scene.backgroundType','error':'Unknown/unreadable background mode; conditional getters not read'}) + result['scene']['backgroundEnvironment'] = result['scene']['backgroundSolidColor'] = skipped('Unknown background mode') + environments, result['environments']['object'] = object_read('renderManager.renderEnvironments',lambda:manager.renderEnvironments) + if environments is not None: + count = read('environments.count',lambda:number(environments.count)) + result['environments']['count'] = count + n = count.get('value');result['environments']['items'] = [] + if type(n) is int and n >= 0: + result['environments']['truncated'] = n > max_environments + for index in range(min(n,max_environments)): + env, descriptor = object_read('environment.' + str(index),lambda index=index:environments.item(index)) + if env is not None: + fields(descriptor,'environment.'+str(index),env,[('id',text,None,None),('name',text,None,None),('isCustomEnvironment',boolean,None,None)]) + result['environments']['items'].append(descriptor) + else: result['errors'].append({'field':'environments.count','error':'Environment count is not a nonnegative integer'}) + result['documentAfter'] = {'modified':read('document.modified.after',lambda:boolean(document.isModified)), + 'saved':read('document.saved.after',lambda:boolean(document.isSaved)), + 'stillActive':read('document.active.after',lambda:boolean(app.activeDocument == document))} + result['documentFlagsUnchanged'] = (result['documentBefore']['modified'].get('available') is True + and result['documentBefore']['saved'].get('available') is True + and result['documentBefore']['modified'] == result['documentAfter']['modified'] + and result['documentBefore']['saved'] == result['documentAfter']['saved'] + and result['documentAfter']['stillActive'].get('value') is True) + classes_complete = all(row.get('available') is True and all(row.get('members',{}).values()) for row in result['classes'].values()) + result['observationComplete'] = not result['errors'] and classes_complete and not result['environments'].get('truncated',False) + need(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES,'Render probe output exceeds byte budget') + return result + + +require(platform.node().lower() == REQUEST['host'].lower(), 'Host identity differs from expected host') +require(re.fullmatch(r'[A-Za-z0-9_-]{12,100}', REQUEST['runId']) is not None, 'Invalid run ID') +modules = [m for n,m in list(sys.modules.items()) + if n.endswith('.mechanical.core') and getattr(m,'SESSION_ID',None) == REQUEST['sessionId']] +require(len(modules) == 1, 'Exact serving mechanical session is unavailable') +registries = getattr(builtins,'_adom_mechanical_ralph_runs',None) +require(isinstance(registries,dict), 'Native ownership registry unavailable') +run = registries.get(REQUEST['runId']) +require(isinstance(run,dict) and run.get('sessionId') == REQUEST['sessionId'], 'Native registry/session missing') +require(isinstance(run.get('documents'),dict), 'Native ownership documents unavailable') +require(isinstance(run.get('baselineObjects'),list) and len(run['baselineObjects']) <= 128, + 'Baseline object inventory exceeds probe budget or is unavailable') +document_count = app.documents.count +require(type(document_count) is int and 0 <= document_count <= 128, 'Open document inventory exceeds probe budget') +row = owned(REQUEST['documentId']) +document = row['object'] +require(app.activeDocument == document, 'Owned render probe document must already be active') +design = adsk.fusion.Design.cast(document.products.itemByProductType('DesignProductType')) +result = {'success':True, 'documentId':REQUEST['documentId'], 'ownerRunId':REQUEST['runId'], + 'sessionId':REQUEST['sessionId'], 'acceptance':'DIAGNOSTIC_ONLY', + 'renderProbe':render_capability_probe(app,document,design)} +require(len(json.dumps(result,allow_nan=False).encode()) <= MAX_OUTPUT_BYTES, + 'Whole render diagnostic output exceeds byte budget') diff --git a/docs/mechanical-verification/cp9-readonly/ownership.json b/docs/mechanical-verification/cp9-readonly/ownership.json new file mode 100644 index 0000000..316faa2 --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/ownership.json @@ -0,0 +1,100 @@ +{ + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "target": "adom_drew2", + "state": "complete", + "documents": { + "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a": { + "role": "01-scene_diagnostic", + "metadata": { + "creationId": "4f1af11b-0a58-4348-b81a-87502fd79fb2", + "name": "Untitled", + "closed": true + }, + "checkpoint": { + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "verified": true + } + }, + "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd": { + "role": "01-scene_diagnostic-reopened", + "metadata": { + "creationId": "6f7c8295-af36-44c4-bbbc-fcdff5932a5e", + "name": "Untitled", + "closed": true + }, + "checkpoint": { + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "verified": true + } + } + }, + "files": { + "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d": { + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "retain": true + } + }, + "preferences": {}, + "customCleanup": [], + "cleanup": [ + { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "success": true + } + } + ], + "hostScratch": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed", + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "host": "ADOM_DREW2", + "lastDispatch": { + "op": "export", + "format": "f3d", + "outputPath": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + }, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "operationId": "20261005T213050Z-422781656d64490da02934d1757322ed-fc37d455ff35" + }, + "pendingRegistrations": {}, + "cleanupLatest": { + "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a": { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd": { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "success": true + } + } + } +} \ No newline at end of file diff --git a/docs/mechanical-verification/cp9-readonly/report.json b/docs/mechanical-verification/cp9-readonly/report.json new file mode 100644 index 0000000..0f6bafb --- /dev/null +++ b/docs/mechanical-verification/cp9-readonly/report.json @@ -0,0 +1,1663 @@ +{ + "runId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "target": "adom_drew2", + "host": "ADOM_DREW2", + "native": "PASS", + "cleanup": "PASS", + "visual": { + "status": "NOT_REQUESTED", + "reason": "This diagnostic forbids images; no visual acceptance claim." + }, + "overall": "PASS_DIAGNOSTIC", + "checks": [ + { + "label": "Diagnostic box complete healthy timeline", + "status": "PASS", + "evidence": { + "complete": true, + "computeSucceeded": null, + "faults": [], + "healthy": true, + "suppressed": [], + "timelineCount": 2, + "unknown": [] + } + }, + { + "label": "Diagnostic box native volume", + "status": "PASS", + "evidence": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + } + }, + { + "label": "Diagnostic box independent instance volume", + "status": "PASS", + "evidence": 6000.0 + }, + { + "label": "Diagnostic box native instance count", + "status": "PASS", + "evidence": 1 + }, + { + "label": "Diagnostic box public and native physical properties", + "status": "PASS", + "evidence": { + "area": 2200.0, + "averageDensity": 2.7e-06, + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "entity": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "objectType": "adsk::fusion::Component", + "occurrencePath": null, + "occurrenceToken": null, + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed", + "rootComponentId": "3400b1c0-7442-4474-9ad7-c5e6343280a7", + "token": "/v4BAAEAAwAAAAAAAAAAAAAA" + }, + "mass": 0.0162, + "materialBasis": "Fusion assigned/default material; supplier provenance does not certify modeled material density", + "scope": "component_subtree", + "space": "component", + "target": { + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "revision": "3e04e18a082fd7bbd2c9add7fb46f2f95ef9ee389aef4398cabb73fa095871ed" + }, + "units": { + "area": "mm2", + "averageDensity": "kg/mm3", + "centerOfMass": "mm", + "mass": "kg", + "volume": "mm3" + }, + "volume": 6000.0 + } + }, + { + "label": "Independent fixture model witness unchanged across probe", + "status": "PASS", + "evidence": null + }, + { + "label": "Reviewed probe remains an owned read-only diagnostic", + "status": "PASS", + "evidence": { + "qualification": "PARTIAL", + "sceneWitnessComplete": false, + "renderingQualified": false, + "nativeQueueIdle": "UNVERIFIED", + "exclusiveSettingsOwnership": "UNVERIFIED", + "fieldErrors": [ + { + "error": "Native object absent or invalid", + "field": "camera" + } + ], + "gaps": [ + "No render execution or RenderFuture instance was created.", + "No global renderer queue/coexistence proof.", + "Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.", + "Environment IDs and names do not prove custom environment content identity.", + "No admitted revision-to-pixels or full source snapshot claim." + ] + } + }, + { + "label": "01-scene_diagnostic archive preserves native witness", + "status": "PASS", + "evidence": { + "bytes": 65067, + "path": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764" + } + }, + { + "label": "Original documents restored", + "status": "PASS", + "evidence": { + "activeCreationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "documents": [ + { + "creationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "modified": false, + "name": "Untitled", + "saved": false + } + ], + "success": true + } + }, + { + "label": "Preferences preserved", + "status": "PASS", + "evidence": { + "activeTheme": "darkblue", + "cameraPivot": true, + "gestureNav": true, + "invertScrollZoom": true, + "lengthUnit": "mm", + "modelingOrientation": "zup", + "orbitScheme": "fusion360", + "theme": "device" + } + }, + { + "label": "Runtime configuration and development pin preserved", + "status": "PASS", + "evidence": { + "name": "fusion360", + "devPinned": true, + "paused": false, + "runningVersion": "1.12.0-dev.5", + "installedVersion": "1.12.0-dev.8", + "entryMismatch": false, + "instanceCount": 1 + } + }, + { + "label": "Actual native runtime identity preserved", + "status": "PASS", + "evidence": { + "image": "adom-fusion-bridge.exe", + "listenerAddresses": [ + "127.0.0.1" + ], + "executablePath": "C:\\Users\\drew\\AppData\\Local\\Adom Bridge\\bridges-cache\\fusion360\\adom-fusion-bridge.exe", + "stable": true, + "processCreatedUtc": "2026-10-05T21:01:31.6721740Z", + "host": "ADOM_DREW2", + "health": { + "status": "ok", + "busy": false, + "longOp": null, + "led": "green", + "summary": "Fusion ready", + "tooltip": "Fusion 360 running, AdomBridge add-in connected. All fusion_* verbs available.", + "bridgeVersion": "1.12.0-dev.8", + "fusion": { + "installed": true, + "exe_path": "C:\\Users\\drew\\AppData\\Local\\Autodesk\\webdeploy\\production\\6a0c9611291d45bb9226980209917c3d\\FusionLauncher.exe", + "addins_dir": "C:\\Users\\drew\\AppData\\Roaming\\Autodesk\\FusionAddins", + "addin_installed": true, + "running": true + }, + "addin": { + "busy": false, + "version": "1.12.0-dev.8", + "pyThreads": 4 + }, + "shell": "rust", + "needsSignin": false, + "licensingDialog": false, + "windowTitle": "Untitled - Autodesk Fusion ", + "windowFactsDegraded": false, + "windowFactsError": null, + "exePort": 50618 + }, + "pid": 42780, + "port": 50618 + } + } + ], + "artifacts": [ + { + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/artifacts/01-scene_diagnostic-fad76a63.f3d", + "sha256": "239216a650721a4fb104c4cdb36d697afd4480d1be4b22f6fe3ab6f971ed3764", + "bytes": 65067, + "kind": "native-archive", + "hostPath": "C:/Users/drew/AppData/Local/Temp/adom-fusion-ralph/20261005T213050Z-422781656d64490da02934d1757322ed/01-scene_diagnostic-fad76a63.f3d", + "fixture": "01-scene_diagnostic" + } + ], + "fixtures": [ + { + "name": "01-scene_diagnostic", + "status": "PASS", + "checks": 8 + } + ], + "requestedFixtures": [ + "01-scene_diagnostic" + ], + "limitations": [ + "Read-only capability/scene field diagnostic only; partial availability is retained.", + "No render future, scene/settings edit, global native queue or complete-scene proof.", + "No capture, screenshot, image artifact or Shotlog path is enabled." + ], + "diagnosticSources": [ + { + "name": "ralph_jobs.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_jobs.py", + "sha256": "4322d735104fb82f722fe03a1a806f6cc64fb264541ef22968111df14150a56f" + }, + { + "name": "ralph_mechanical.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_mechanical.py", + "sha256": "ab7451dbc3dda32442128a6262aebf3b9b53884571c6174e9bc2151e5f912d2e" + }, + { + "name": "ralph_support.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/ralph_support.py", + "sha256": "a152fed6766ba0e86f89a84b52ce9090bcabcbc3bf888ae1b7f7b08836895e27" + }, + { + "name": "fusion_harness.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/fusion_harness.py", + "sha256": "0ded009f2ad0b5d8919ac215b9366df2b44d9ef98416e5f4a076e36656f88ec2" + }, + { + "name": "probe/read_scene_probe.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/read_scene_probe.py", + "sha256": "bc2bcf78675f28151faa4ab0c67276e553792cc0c6c206fad98718219e02814c" + }, + { + "name": "probe/test_read_scene_probe.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/test_read_scene_probe.py", + "sha256": "8df35f06355717a0c8bfd64b66a951a4a349b9257aff0b14ff1db7a02edb8ba4" + }, + { + "name": "probe/READONLY-PROBE.md", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/probe/READONLY-PROBE.md", + "sha256": "01f33dde1a9bfa821b308a91d0889cadce7c6098af73cdcfbe98e66bc527ff41" + }, + { + "name": "diagnostic_scene.py", + "path": "/home/adom/project/research/fusion-mechanical-verification-2026-10-02/cp9-readonly-native-1/diagnostic-sources-20261005T213050Z-422781656d64490da02934d1757322ed/diagnostic_scene.py", + "sha256": "bad536a1d0159eb5e2426e5e671510bc3e0178aee80866f477152f43c3b02073" + } + ], + "acceptanceScope": { + "sceneDiagnostic": "PASS", + "rendering": "NOT_RUN", + "futureLifecycle": "NOT_RUN", + "sceneCompleteness": "NOT_PROVEN", + "queueOwnership": "NOT_PROVEN", + "visual": "NOT_REQUESTED" + }, + "probeAttempted": true, + "baseline": { + "state": { + "_hint": "Fusion is clear. The screenshot shows the current surface - this is the verb to render any fusion_show_* result (they open in the background by design).", + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "activeDocument": "Untitled", + "addinResponding": true, + "blockingDialogs": [], + "dialogs": [], + "drivingBadge": { + "active": false, + "enabled": true, + "idleShedSeconds": 300, + "lastDrivingVerbAt": 1791234613.3465655, + "lastError": null + }, + "durationMs": 253, + "error": "", + "focusEvents": [], + "hwnd": 4658928, + "mainThreadBusy": null, + "output": "{\"running\":true,\"activeDocument\":\"Untitled\",\"mainThreadBusy\":null,\"blockingDialogs\":[],\"screenshot\":null}", + "rect": { + "bottom": 1549, + "height": 1549, + "left": -11, + "right": 2572, + "top": 0, + "width": 2583 + }, + "running": true, + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60, + "timings": { + "launch": { + "cold": { + "count": 10, + "lastAt": 1791234147, + "p50": 41.1, + "p90": 216.2 + }, + "warm": { + "count": 2, + "lastAt": 1791230617, + "p50": 0.0, + "p90": 1.0 + } + }, + "verb:activate_document": { + "any": { + "count": 3, + "lastAt": 1789145672, + "p50": 3.1, + "p90": 5.6 + } + }, + "verb:addin_status": { + "any": { + "count": 3, + "lastAt": 1788899767, + "p50": 2.0, + "p90": 2.0 + } + }, + "verb:aps_browse": { + "any": { + "count": 4, + "lastAt": 1789143968, + "p50": 1.9, + "p90": 2.3 + } + }, + "verb:aps_file_info": { + "any": { + "count": 10, + "lastAt": 1789144745, + "p50": 34.8, + "p90": 36.2 + } + }, + "verb:aps_get_browser": { + "any": { + "count": 1, + "lastAt": 1787944552, + "p50": 0.0, + "p90": 0.0 + } + }, + "verb:aps_open": { + "any": { + "count": 5, + "lastAt": 1788196241, + "p50": 0.2, + "p90": 37.2 + } + }, + "verb:aps_search": { + "any": { + "count": 10, + "lastAt": 1789684900, + "p50": 35.7, + "p90": 65.6 + } + }, + "verb:aps_signin": { + "any": { + "count": 2, + "lastAt": 1787949063, + "p50": 0.4, + "p90": 1.1 + } + }, + "verb:aps_status": { + "any": { + "count": 10, + "lastAt": 1788899782, + "p50": 0.0, + "p90": 0.0 + } + }, + "verb:assembly_bom": { + "any": { + "count": 3, + "lastAt": 1790955710, + "p50": 0.1, + "p90": 0.6 + } + }, + "verb:check_dialogs": { + "any": { + "count": 1, + "lastAt": 1788195388, + "p50": 1.4, + "p90": 1.4 + } + }, + "verb:close_document": { + "any": { + "count": 9, + "lastAt": 1789145809, + "p50": 6.7, + "p90": 46.1 + } + }, + "verb:describe": { + "any": { + "count": 3, + "lastAt": 1789144008, + "p50": 0.0, + "p90": 0.0 + } + }, + "verb:document_info": { + "any": { + "count": 10, + "lastAt": 1791234611, + "p50": 0.1, + "p90": 0.2 + } + }, + "verb:eagle_import_status": { + "any": { + "count": 1, + "lastAt": 1790978404, + "p50": 1.8, + "p90": 1.8 + } + }, + "verb:electron_run": { + "any": { + "count": 10, + "lastAt": 1788456727, + "p50": 6.3, + "p90": 13.3 + } + }, + "verb:export_cloud_file": { + "any": { + "count": 4, + "lastAt": 1789685179, + "p50": 223.4, + "p90": 290.3 + } + }, + "verb:export_eagle_source": { + "any": { + "count": 10, + "lastAt": 1788456713, + "p50": 1.7, + "p90": 2.2 + } + }, + "verb:export_f3d": { + "any": { + "count": 2, + "lastAt": 1790955227, + "p50": 3.7, + "p90": 4.4 + } + }, + "verb:export_obj": { + "any": { + "count": 2, + "lastAt": 1787944913, + "p50": 0.7, + "p90": 0.7 + } + }, + "verb:export_step": { + "any": { + "count": 10, + "lastAt": 1788456783, + "p50": 5.5, + "p90": 7.9 + } + }, + "verb:get_app_state": { + "any": { + "count": 10, + "lastAt": 1791234147, + "p50": 1.3, + "p90": 3.6 + } + }, + "verb:get_design_info": { + "any": { + "count": 5, + "lastAt": 1788900070, + "p50": 5.2, + "p90": 10.8 + } + }, + "verb:help": { + "any": { + "count": 1, + "lastAt": 1788194522, + "p50": 1.0, + "p90": 1.0 + } + }, + "verb:kill": { + "any": { + "count": 1, + "lastAt": 1787833443, + "p50": 66.9, + "p90": 66.9 + } + }, + "verb:list_cloud_files": { + "any": { + "count": 1, + "lastAt": 1787832975, + "p50": 15.8, + "p90": 15.8 + } + }, + "verb:mcp_call": { + "any": { + "count": 5, + "lastAt": 1788455586, + "p50": 0.0, + "p90": 1.8 + } + }, + "verb:mech_capabilities": { + "any": { + "count": 10, + "lastAt": 1791235757, + "p50": 0.1, + "p90": 0.1 + } + }, + "verb:mech_operation_status": { + "any": { + "count": 10, + "lastAt": 1791229196, + "p50": 0.1, + "p90": 0.1 + } + }, + "verb:mechanical": { + "any": { + "count": 10, + "lastAt": 1791235822, + "p50": 0.8, + "p90": 2.0 + } + }, + "verb:open_by_urn": { + "any": { + "count": 10, + "lastAt": 1789144985, + "p50": 43.4, + "p90": 59.1 + } + }, + "verb:open_cloud_file": { + "any": { + "count": 8, + "lastAt": 1789666231, + "p50": 24.2, + "p90": 87.5 + } + }, + "verb:open_design": { + "any": { + "count": 3, + "lastAt": 1790955721, + "p50": 5.9, + "p90": 6.1 + } + }, + "verb:open_lbr": { + "any": { + "count": 2, + "lastAt": 1788195396, + "p50": 2.7, + "p90": 5.0 + } + }, + "verb:physical_properties": { + "any": { + "count": 3, + "lastAt": 1790955710, + "p50": 0.1, + "p90": 0.2 + } + }, + "verb:readiness": { + "any": { + "count": 5, + "lastAt": 1788900108, + "p50": 14.9, + "p90": 56.9 + } + }, + "verb:restart_for_addin": { + "any": { + "count": 10, + "lastAt": 1791234174, + "p50": 0.0, + "p90": 125.2 + } + }, + "verb:run_modeling_script": { + "any": { + "count": 10, + "lastAt": 1791234623, + "p50": 1.7, + "p90": 1.9 + } + }, + "verb:search_cloud_files": { + "any": { + "count": 3, + "lastAt": 1789143642, + "p50": 0.0, + "p90": 0.4 + } + }, + "verb:show_2d_board": { + "any": { + "count": 2, + "lastAt": 1788456656, + "p50": 26.6, + "p90": 34.9 + } + }, + "verb:show_3d_board": { + "any": { + "count": 6, + "lastAt": 1788456760, + "p50": 10.2, + "p90": 19.6 + } + }, + "verb:signin": { + "any": { + "count": 1, + "lastAt": 1788899909, + "p50": 56.1, + "p90": 56.1 + } + }, + "verb:start": { + "any": { + "count": 7, + "lastAt": 1791230617, + "p50": 57.4, + "p90": 64.9 + } + }, + "verb:status": { + "any": { + "count": 2, + "lastAt": 1788194217, + "p50": 0.8, + "p90": 1.0 + } + }, + "verb:stop": { + "any": { + "count": 1, + "lastAt": 1787831926, + "p50": 19.7, + "p90": 19.7 + } + }, + "verb:window_info": { + "any": { + "count": 10, + "lastAt": 1788899841, + "p50": 7.2, + "p90": 10.9 + } + } + }, + "timingsSentence": "on this machine Fusion starts in cold 41s, warm 0s (measured)", + "title": "Untitled - Autodesk Fusion " + }, + "addin": { + "_timeoutHint": "ab waits up to 60s for this verb before the request times out. If it's a long operation (cloud open / download / export), DON'T treat a timeout as failure \u2014 the work usually keeps running on the bridge. Poll `fusion_get_app_state` until it reports done.", + "addinStale": false, + "addinVersion": "1.12.0-dev.8", + "busy": false, + "durationMs": 68, + "error": "", + "expectedAddinVersion": "1.12.0-dev.8", + "output": "{\"busy\":false,\"version\":\"1.12.0-dev.8\",\"pyThreads\":4,\"addinVersion\":\"1.12.0-dev.8\",\"expectedAddinVersion\":\"1.12.0-dev.8\",\"addinStale\":false}", + "pyThreads": 4, + "schemaVersion": 1, + "shell": "rust", + "status": "ok", + "statusVerb": "fusion_get_app_state", + "success": true, + "timeoutSeconds": 60, + "version": "1.12.0-dev.8" + }, + "runtime": { + "name": "fusion360", + "devPinned": true, + "paused": false, + "runningVersion": "1.12.0-dev.5", + "installedVersion": "1.12.0-dev.8", + "entryMismatch": false, + "instanceCount": 1 + }, + "liveIdentity": { + "image": "adom-fusion-bridge.exe", + "listenerAddresses": [ + "127.0.0.1" + ], + "executablePath": "C:\\Users\\drew\\AppData\\Local\\Adom Bridge\\bridges-cache\\fusion360\\adom-fusion-bridge.exe", + "stable": true, + "processCreatedUtc": "2026-10-05T21:01:31.6721740Z", + "host": "ADOM_DREW2", + "health": { + "status": "ok", + "busy": false, + "longOp": null, + "led": "green", + "summary": "Fusion ready", + "tooltip": "Fusion 360 running, AdomBridge add-in connected. All fusion_* verbs available.", + "bridgeVersion": "1.12.0-dev.8", + "fusion": { + "installed": true, + "exe_path": "C:\\Users\\drew\\AppData\\Local\\Autodesk\\webdeploy\\production\\6a0c9611291d45bb9226980209917c3d\\FusionLauncher.exe", + "addins_dir": "C:\\Users\\drew\\AppData\\Roaming\\Autodesk\\FusionAddins", + "addin_installed": true, + "running": true + }, + "addin": { + "busy": false, + "version": "1.12.0-dev.8", + "pyThreads": 4 + }, + "shell": "rust", + "needsSignin": false, + "licensingDialog": false, + "windowTitle": "Untitled - Autodesk Fusion ", + "windowFactsDegraded": false, + "windowFactsError": null, + "exePort": 50618 + }, + "pid": 42780, + "port": 50618 + }, + "preferences": { + "activeTheme": "darkblue", + "cameraPivot": true, + "gestureNav": true, + "invertScrollZoom": true, + "lengthUnit": "mm", + "modelingOrientation": "zup", + "orbitScheme": "fusion360", + "theme": "device" + }, + "native": { + "activeCommand": "SelectCommand", + "activeCreationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "activeProductType": "DesignProductType", + "documents": [ + { + "creationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "modified": false, + "name": "Untitled", + "saved": false + } + ], + "fusionVersion": "2705.1.30", + "host": "adom_drew2", + "platform": "Windows", + "temporaryDirectory": "C:/Users/drew/AppData/Local/Temp" + } + }, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "modelBeforeProbe": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + }, + "sceneProbe": { + "acceptance": "DIAGNOSTIC_ONLY", + "documentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "ownerRunId": "20261005T213050Z-422781656d64490da02934d1757322ed", + "renderProbe": { + "camera": { + "object": { + "available": false, + "error": "Native object absent or invalid" + } + }, + "classes": { + "RenderEnvironment": { + "available": true, + "members": { + "id": true, + "isCustomEnvironment": true, + "name": true + }, + "methodInvocation": false + }, + "RenderFuture": { + "available": true, + "members": { + "filename": true, + "imageHeight": true, + "imageWidth": true, + "isValid": true, + "progress": true, + "renderState": true + }, + "methodInvocation": false + }, + "RenderManager": { + "available": true, + "members": { + "isRenderWorkspaceActive": true, + "lights": true, + "renderEnvironments": true, + "rendering": true, + "sceneSettings": true + }, + "methodInvocation": false + }, + "Rendering": { + "available": true, + "members": { + "aspectRatio": true, + "isBackgroundTransparent": true, + "renderQuality": true, + "resolution": true, + "resolutionHeight": true, + "resolutionWidth": true, + "startLocalRender": true + }, + "methodInvocation": false + }, + "SceneSettings": { + "available": true, + "members": { + "backgroundEnvironment": true, + "backgroundSolidColor": true, + "backgroundType": true, + "isDepthOfFieldEnabled": true + }, + "methodInvocation": false + } + }, + "documentAfter": { + "modified": { + "available": true, + "value": true + }, + "saved": { + "available": true, + "value": false + }, + "stillActive": { + "available": true, + "value": true + } + }, + "documentBefore": { + "modified": { + "available": true, + "value": true + }, + "saved": { + "available": true, + "value": false + } + }, + "documentFlagsUnchanged": true, + "enums": { + "background": { + "EnvironmentRenderSceneBackgroundType": { + "available": true, + "value": 0 + }, + "SolidColorRenderSceneBackgroundType": { + "available": true, + "value": 1 + } + }, + "camera": { + "OrthographicCameraType": { + "available": true, + "value": 0 + }, + "PerspectiveCameraType": { + "available": true, + "value": 1 + }, + "PerspectiveWithOrthoFacesCameraType": { + "available": true, + "value": 2 + } + } + }, + "environments": { + "count": { + "available": true, + "value": 14 + }, + "items": [ + { + "available": true, + "id": { + "available": true, + "value": "Studio1" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Sharp Highlights" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio2" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Rim Highlights" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio3" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Cool Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio5" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Warm Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio6" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Soft Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "Studio7" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Grid Light" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "CustomLoadedEnvironment" + }, + "isCustomEnvironment": { + "available": true, + "value": true + }, + "name": { + "available": true, + "value": "Custom..." + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "crossroads" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Crossroads" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "DryLakeBed" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Dry lake bed" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "field" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Field" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "photobooth" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Photobooth" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "mi360_Plaza" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Plaza" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "skylight" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Skylight" + }, + "objectType": "adsk::fusion::RenderEnvironment" + }, + { + "available": true, + "id": { + "available": true, + "value": "mi360_Snow" + }, + "isCustomEnvironment": { + "available": true, + "value": false + }, + "name": { + "available": true, + "value": "Snow Field" + }, + "objectType": "adsk::fusion::RenderEnvironment" + } + ], + "object": { + "available": true, + "objectType": "adsk::fusion::RenderEnvironments" + }, + "truncated": false + }, + "errors": [ + { + "error": "Native object absent or invalid", + "field": "camera" + } + ], + "exclusiveSettingsOwnership": "UNVERIFIED", + "gaps": [ + "No render execution or RenderFuture instance was created.", + "No global renderer queue/coexistence proof.", + "Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.", + "Environment IDs and names do not prove custom environment content identity.", + "No admitted revision-to-pixels or full source snapshot claim." + ], + "nativeQueueIdle": "UNVERIFIED", + "observationComplete": false, + "probeVersion": 1, + "renderManager": { + "available": true, + "objectType": "adsk::fusion::RenderManager", + "ownerMatches": { + "available": true, + "value": true + }, + "workspaceActive": { + "available": true, + "value": false + } + }, + "renderStarted": false, + "rendering": { + "aspectRatio": { + "available": true, + "unit": "native RenderAspectRatios enum", + "value": 0 + }, + "isBackgroundTransparent": { + "available": true, + "value": false + }, + "object": { + "available": true, + "objectType": "adsk::fusion::Rendering" + }, + "renderQuality": { + "available": true, + "unit": "native quality (25..100)", + "value": 75 + }, + "resolution": { + "available": true, + "unit": "native RenderResolutions enum", + "value": 17 + }, + "resolutionHeight": { + "available": true, + "unit": "px", + "value": 1242 + }, + "resolutionWidth": { + "available": true, + "unit": "px", + "value": 1961 + } + }, + "scene": { + "aspectRatio": { + "available": true, + "unit": "native RenderAspectRatios enum", + "value": 0 + }, + "aspectRatioHeight": { + "available": true, + "unit": "ratio", + "value": 0 + }, + "aspectRatioWidth": { + "available": true, + "unit": "ratio", + "value": 0 + }, + "backgroundEnvironment": { + "available": null, + "notRead": "Solid background" + }, + "backgroundSolidColor": { + "available": true, + "unit": "RGBA 0..255; opacity ignored by scene", + "value": { + "blue": 178, + "green": 178, + "opacity": 255, + "red": 178 + } + }, + "backgroundType": { + "available": true, + "unit": "native RenderSceneBackgroundTypes enum", + "value": 1 + }, + "brightness": { + "available": true, + "unit": "lux", + "value": 2000.0 + }, + "cameraExposure": { + "available": true, + "unit": "EV", + "value": 12.0 + }, + "cameraFocalLength": { + "available": true, + "nativeUnit": "mm", + "unit": "mm", + "value": 90.0 + }, + "cameraType": { + "available": true, + "unit": "native CameraTypes enum", + "value": 0 + }, + "centerOfFocus": { + "available": null, + "notRead": "isDepthOfFieldEnabled is false or unreadable" + }, + "depthOfFieldBlur": { + "available": null, + "notRead": "isDepthOfFieldEnabled is false or unreadable" + }, + "groundOffset": { + "available": true, + "nativeUnit": "cm", + "unit": "mm", + "value": 0.0 + }, + "groundPosition": { + "available": null, + "notRead": "isGroundFlattened is false or unreadable" + }, + "groundRoughness": { + "available": null, + "notRead": "isGroundReflections is false or unreadable" + }, + "isDepthOfFieldEnabled": { + "available": true, + "value": false + }, + "isGroundDisplayed": { + "available": true, + "value": true + }, + "isGroundFlattened": { + "available": true, + "value": false + }, + "isGroundReflections": { + "available": true, + "value": false + }, + "lightAngle": { + "available": true, + "nativeUnit": "rad", + "unit": "deg", + "value": -0.0 + }, + "object": { + "available": true, + "objectType": "adsk::fusion::SceneSettings" + } + }, + "sceneWitnessComplete": false, + "settingsWritten": false, + "status": "DIAGNOSTIC_ONLY", + "viewport": { + "available": true, + "objectType": "adsk::core::Viewport" + } + }, + "sessionId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c", + "success": true + }, + "modelAfterProbe": { + "components": [ + { + "attributes": { + "requestedName": "Untitled" + }, + "bodies": [ + { + "bounds": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 20.0, + 30.0, + 10.0 + ] + ], + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "faces": 6, + "mass": 0.0162, + "material": "Aluminum", + "name": "Block", + "volume": 6000.0 + } + ], + "description": "", + "material": "Aluminum", + "name": "", + "partNumber": "2026-10-05-14-31-00-614", + "sketches": [ + { + "curves": 4, + "fullyConstrained": false, + "name": "Base rectangle", + "profiles": 1 + } + ] + } + ], + "instanceCount": 1, + "instanceVolume": 6000.0, + "occurrences": [], + "parameters": { + "d5": "width", + "d6": "height", + "d7": "depth", + "d8": "0.0 deg", + "depth": "10.0 mm", + "height": "30.0 mm", + "width": "20.0 mm" + }, + "physical": { + "centerOfMass": [ + 10.0, + 15.0, + 5.0 + ], + "mass": 0.0162, + "volume": 6000.0 + }, + "timelineCount": 2 + }, + "diagnosticQualification": { + "qualification": "PARTIAL", + "sceneWitnessComplete": false, + "renderingQualified": false, + "nativeQueueIdle": "UNVERIFIED", + "exclusiveSettingsOwnership": "UNVERIFIED", + "fieldErrors": [ + { + "error": "Native object absent or invalid", + "field": "camera" + } + ], + "gaps": [ + "No render execution or RenderFuture instance was created.", + "No global renderer queue/coexistence proof.", + "Appearance, material, texture, light and occurrence-visibility graphs are outside this bounded probe.", + "Environment IDs and names do not prove custom environment content identity.", + "No admitted revision-to-pixels or full source snapshot claim." + ] + }, + "cleanupEvidence": [ + { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "success": true + } + } + ], + "cleanupLatest": { + "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a": { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:cdfc43c8-2450-4f1d-b2f2-716ad0c43f9a", + "success": true + } + }, + "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd": { + "label": "close owned document dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "status": "PASS", + "errors": [], + "raw": { + "closed": true, + "closedDocumentId": "dd15182e-3fa9-4ea3-bc57-4175e60ef00c:39b22ab5-617b-4260-b619-670047931ecd", + "success": true + } + } + }, + "finalRuntimeIdentity": { + "image": "adom-fusion-bridge.exe", + "listenerAddresses": [ + "127.0.0.1" + ], + "executablePath": "C:\\Users\\drew\\AppData\\Local\\Adom Bridge\\bridges-cache\\fusion360\\adom-fusion-bridge.exe", + "stable": true, + "processCreatedUtc": "2026-10-05T21:01:31.6721740Z", + "host": "ADOM_DREW2", + "health": { + "status": "ok", + "busy": false, + "longOp": null, + "led": "green", + "summary": "Fusion ready", + "tooltip": "Fusion 360 running, AdomBridge add-in connected. All fusion_* verbs available.", + "bridgeVersion": "1.12.0-dev.8", + "fusion": { + "installed": true, + "exe_path": "C:\\Users\\drew\\AppData\\Local\\Autodesk\\webdeploy\\production\\6a0c9611291d45bb9226980209917c3d\\FusionLauncher.exe", + "addins_dir": "C:\\Users\\drew\\AppData\\Roaming\\Autodesk\\FusionAddins", + "addin_installed": true, + "running": true + }, + "addin": { + "busy": false, + "version": "1.12.0-dev.8", + "pyThreads": 4 + }, + "shell": "rust", + "needsSignin": false, + "licensingDialog": false, + "windowTitle": "Untitled - Autodesk Fusion ", + "windowFactsDegraded": false, + "windowFactsError": null, + "exePort": 50618 + }, + "pid": 42780, + "port": 50618 + }, + "finalHost": { + "activeCreationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "documents": [ + { + "creationId": "cdf1e4ec-a727-4ffe-8975-d10e11191864", + "modified": false, + "name": "Untitled", + "saved": false + } + ], + "success": true + }, + "counts": { + "PASS": 12, + "FAIL": 0 + } +}