asecli 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asecli/__init__.py +3 -0
- asecli/bridge/__init__.py +40 -0
- asecli/bridge/_editor_port_contract.py +131 -0
- asecli/bridge/_editor_primitives.py +205 -0
- asecli/bridge/_editor_spec_grammar.py +245 -0
- asecli/bridge/_editor_spec_io.py +26 -0
- asecli/bridge/_editor_spec_validation.py +218 -0
- asecli/bridge/_gui_handoff_store.py +78 -0
- asecli/bridge/_gui_project.py +35 -0
- asecli/bridge/_gui_resource_store.py +159 -0
- asecli/bridge/_gui_resource_upgrade.py +122 -0
- asecli/bridge/editor_create.py +173 -0
- asecli/bridge/editor_spec.py +250 -0
- asecli/bridge/graph_geometry.py +249 -0
- asecli/bridge/graph_geometry_parser.py +57 -0
- asecli/bridge/graph_inspect.py +136 -0
- asecli/bridge/gui_handoff.py +100 -0
- asecli/bridge/gui_presentation.py +70 -0
- asecli/bridge/gui_provider_detection.py +197 -0
- asecli/bridge/gui_runtime_probe.py +25 -0
- asecli/bridge/gui_support.py +240 -0
- asecli/bridge/gui_support_resource.py +58 -0
- asecli/bridge/mcp_client.py +231 -0
- asecli/bridge/recompile.py +107 -0
- asecli/bridge/resource_text.py +12 -0
- asecli/bridge/resources/asecli_material_gui.authoring.part00.cs.txt +238 -0
- asecli/bridge/resources/asecli_material_gui.authoring.part01.cs.txt +235 -0
- asecli/bridge/resources/asecli_material_gui.authoring.store.cs.txt +125 -0
- asecli/bridge/resources/asecli_material_gui.condition.cs.txt +99 -0
- asecli/bridge/resources/asecli_material_gui.hydration.cs.txt +105 -0
- asecli/bridge/resources/asecli_material_gui.part00.cs.txt +300 -0
- asecli/bridge/resources/asecli_material_gui.part01.cs.txt +290 -0
- asecli/bridge/resources/asecli_material_gui.reconciliation.cs.txt +174 -0
- asecli/bridge/resources/asecli_material_gui.transaction.cs.txt +108 -0
- asecli/bridge/resources/editor_create.part00.cs.txt +176 -0
- asecli/bridge/resources/editor_create.part01.cs.txt +145 -0
- asecli/bridge/resources/editor_create.part02.cs.txt +139 -0
- asecli/bridge/resources/editor_create.part03.cs.txt +161 -0
- asecli/bridge/resources/editor_create.part04.cs.txt +114 -0
- asecli/bridge/resources/wire_route.transaction.cs.txt +140 -0
- asecli/bridge/wire_route.py +127 -0
- asecli/checks/__init__.py +10 -0
- asecli/checks/checksum.py +53 -0
- asecli/checks/local_vars.py +135 -0
- asecli/checks/usage.py +141 -0
- asecli/checks/validate.py +129 -0
- asecli/cli/__init__.py +1 -0
- asecli/cli/commands.py +237 -0
- asecli/cli/commentary_command.py +158 -0
- asecli/cli/create_command.py +250 -0
- asecli/cli/custom_gui_command.py +170 -0
- asecli/cli/gui_provider.py +30 -0
- asecli/cli/gui_support_command.py +35 -0
- asecli/cli/io.py +214 -0
- asecli/cli/layout_command.py +213 -0
- asecli/cli/main.py +245 -0
- asecli/cli/recompile_metadata.py +53 -0
- asecli/cli/skill_command.py +121 -0
- asecli/cli/usage_command.py +42 -0
- asecli/core/__init__.py +84 -0
- asecli/core/comment_bounds.py +166 -0
- asecli/core/comment_layout.py +167 -0
- asecli/core/comment_metrics.py +132 -0
- asecli/core/comment_purpose.py +140 -0
- asecli/core/commentary.py +225 -0
- asecli/core/compiled_metadata.py +135 -0
- asecli/core/compiled_properties.py +190 -0
- asecli/core/custom_gui.py +250 -0
- asecli/core/custom_gui_versions.py +35 -0
- asecli/core/fishbone_placement.py +159 -0
- asecli/core/fishbone_topology.py +186 -0
- asecli/core/graph_ops.py +122 -0
- asecli/core/layout.py +174 -0
- asecli/core/layout_audit.py +237 -0
- asecli/core/layout_audit_geometry.py +247 -0
- asecli/core/layout_audit_repeated.py +44 -0
- asecli/core/layout_graph.py +113 -0
- asecli/core/local_vars.py +54 -0
- asecli/core/material_gui_condition.py +68 -0
- asecli/core/material_gui_protocol.py +35 -0
- asecli/core/material_gui_spec.py +191 -0
- asecli/core/meticulous_layout.py +114 -0
- asecli/core/model.py +237 -0
- asecli/core/property_presentation.py +233 -0
- asecli/core/wire_geometry.py +83 -0
- asecli/core/wire_router.py +248 -0
- asecli/schema/__init__.py +49 -0
- asecli/schema/data/observed.json +16 -0
- asecli/schema/data/schemas.json +14106 -0
- asecli/skills/asecli/SKILL.md +275 -0
- asecli/skills/asecli/references/layout-standard.md +99 -0
- asecli/skills/asecli/references/master-output-settings-standard.md +51 -0
- asecli/skills/asecli/references/material-property-standard.md +108 -0
- asecli-0.6.2.dist-info/METADATA +594 -0
- asecli-0.6.2.dist-info/RECORD +98 -0
- asecli-0.6.2.dist-info/WHEEL +4 -0
- asecli-0.6.2.dist-info/entry_points.txt +2 -0
- asecli-0.6.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
string moveError = UnityEditor.AssetDatabase.MoveAsset(temporaryAssetPath, assetPath);
|
|
3
|
+
if (!string.IsNullOrEmpty(moveError)) throw new System.Exception("asset commit failed: " + moveError);
|
|
4
|
+
committed = true;
|
|
5
|
+
var targetShader = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Shader>(assetPath);
|
|
6
|
+
if (targetShader == null || targetShader.name != shaderName)
|
|
7
|
+
throw new System.Exception("shader identity changed after asset commit");
|
|
8
|
+
|
|
9
|
+
var result = new Newtonsoft.Json.Linq.JObject();
|
|
10
|
+
result["protocol"] = "ASECLI_EDITOR_CREATE_V1";
|
|
11
|
+
result["ase_version"] = aseVersion;
|
|
12
|
+
result["asset_path"] = assetPath;
|
|
13
|
+
result["template_guid"] = reloadedMaster.CurrentTemplate.GUID;
|
|
14
|
+
result["shader_name"] = targetShader.name;
|
|
15
|
+
result["saved"] = saved;
|
|
16
|
+
result["reloaded"] = reloaded;
|
|
17
|
+
result["committed"] = committed;
|
|
18
|
+
var manifest = new Newtonsoft.Json.Linq.JObject();
|
|
19
|
+
var manifestTemplate = new Newtonsoft.Json.Linq.JObject();
|
|
20
|
+
manifestTemplate["guid"] = reloadedMaster.CurrentTemplate.GUID;
|
|
21
|
+
manifestTemplate["shader_name"] = targetShader.name;
|
|
22
|
+
manifest["template"] = manifestTemplate;
|
|
23
|
+
manifest["nodes"] = manifestNodes;
|
|
24
|
+
manifest["connections"] = manifestConnections;
|
|
25
|
+
result["manifest"] = manifest;
|
|
26
|
+
|
|
27
|
+
UnityEditor.Selection.activeObject = previousSelection;
|
|
28
|
+
AmplifyShaderEditor.UIUtils.CurrentWindow = previousWindow;
|
|
29
|
+
stateRestored = true;
|
|
30
|
+
if (win != null && win != previousWindow)
|
|
31
|
+
{
|
|
32
|
+
var createdWindow = win;
|
|
33
|
+
createdWindow.Close();
|
|
34
|
+
UnityEngine.Object.DestroyImmediate(createdWindow);
|
|
35
|
+
win = null;
|
|
36
|
+
}
|
|
37
|
+
success = true;
|
|
38
|
+
return "ASECLI_EDITOR_CREATE_V1:" + result.ToString(Newtonsoft.Json.Formatting.None);
|
|
39
|
+
}
|
|
40
|
+
catch (System.Exception ex)
|
|
41
|
+
{
|
|
42
|
+
failure = ex;
|
|
43
|
+
throw;
|
|
44
|
+
}
|
|
45
|
+
finally
|
|
46
|
+
{
|
|
47
|
+
var cleanupErrors = new System.Collections.Generic.List<string>();
|
|
48
|
+
if (win == null && AmplifyShaderEditor.UIUtils.CurrentWindow != previousWindow)
|
|
49
|
+
win = AmplifyShaderEditor.UIUtils.CurrentWindow as AmplifyShaderEditor.AmplifyShaderEditorWindow;
|
|
50
|
+
if (!success)
|
|
51
|
+
{
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
if (committed && !UnityEditor.AssetDatabase.DeleteAsset(assetPath) &&
|
|
55
|
+
System.IO.File.Exists(System.IO.Path.GetFullPath(assetPath)))
|
|
56
|
+
cleanupErrors.Add("failed to roll back committed target asset");
|
|
57
|
+
}
|
|
58
|
+
catch (System.Exception cleanupEx)
|
|
59
|
+
{
|
|
60
|
+
cleanupErrors.Add("target rollback threw: " + cleanupEx.Message);
|
|
61
|
+
}
|
|
62
|
+
try
|
|
63
|
+
{
|
|
64
|
+
if (!UnityEditor.AssetDatabase.DeleteAsset(temporaryAssetPath) &&
|
|
65
|
+
System.IO.File.Exists(System.IO.Path.GetFullPath(temporaryAssetPath)))
|
|
66
|
+
cleanupErrors.Add("failed to delete temporary asset");
|
|
67
|
+
}
|
|
68
|
+
catch (System.Exception cleanupEx)
|
|
69
|
+
{
|
|
70
|
+
cleanupErrors.Add("temporary cleanup threw: " + cleanupEx.Message);
|
|
71
|
+
}
|
|
72
|
+
try { UnityEditor.AssetDatabase.Refresh(); }
|
|
73
|
+
catch (System.Exception cleanupEx) { cleanupErrors.Add("AssetDatabase.Refresh threw: " + cleanupEx.Message); }
|
|
74
|
+
}
|
|
75
|
+
if (!stateRestored)
|
|
76
|
+
{
|
|
77
|
+
try
|
|
78
|
+
{
|
|
79
|
+
UnityEditor.Selection.activeObject = previousSelection;
|
|
80
|
+
AmplifyShaderEditor.UIUtils.CurrentWindow = previousWindow;
|
|
81
|
+
stateRestored = true;
|
|
82
|
+
}
|
|
83
|
+
catch (System.Exception cleanupEx)
|
|
84
|
+
{
|
|
85
|
+
cleanupErrors.Add("Editor state restore threw: " + cleanupEx.Message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (win != null && win != previousWindow)
|
|
89
|
+
{
|
|
90
|
+
try
|
|
91
|
+
{
|
|
92
|
+
win.Close();
|
|
93
|
+
UnityEngine.Object.DestroyImmediate(win);
|
|
94
|
+
win = null;
|
|
95
|
+
}
|
|
96
|
+
catch (System.Exception cleanupEx)
|
|
97
|
+
{
|
|
98
|
+
cleanupErrors.Add("window cleanup threw: " + cleanupEx.Message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (cleanupErrors.Count > 0)
|
|
102
|
+
{
|
|
103
|
+
string cleanupMessage = string.Join(" | ", cleanupErrors.ToArray());
|
|
104
|
+
if (failure != null)
|
|
105
|
+
{
|
|
106
|
+
failure.Data["ASECLI cleanup failures"] = cleanupMessage;
|
|
107
|
+
UnityEngine.Debug.LogError("ASECLI Editor create cleanup failures: " + cleanupMessage);
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
{
|
|
111
|
+
throw new System.Exception("ASECLI Editor create cleanup failed: " + cleanupMessage);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
var payloadJson = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String("{payload_base64}"));
|
|
2
|
+
var payload = Newtonsoft.Json.Linq.JObject.Parse(payloadJson);
|
|
3
|
+
string assetPath = (string)payload["asset_path"];
|
|
4
|
+
var routes = (Newtonsoft.Json.Linq.JArray)payload["routes"];
|
|
5
|
+
var previousWindow = AmplifyShaderEditor.UIUtils.CurrentWindow;
|
|
6
|
+
var previousSelection = UnityEditor.Selection.activeObject;
|
|
7
|
+
AmplifyShaderEditor.AmplifyShaderEditorWindow win = null;
|
|
8
|
+
byte[] diskSnapshot = null;
|
|
9
|
+
bool saved = false;
|
|
10
|
+
try
|
|
11
|
+
{
|
|
12
|
+
if ((int)payload["version"] != 1)
|
|
13
|
+
throw new System.Exception("unsupported WireNode route version");
|
|
14
|
+
if (string.IsNullOrEmpty(assetPath) || !assetPath.StartsWith("Assets/") ||
|
|
15
|
+
!assetPath.EndsWith(".shader") || assetPath.Contains("..") || assetPath.Contains("\\"))
|
|
16
|
+
throw new System.Exception("unsafe target asset path");
|
|
17
|
+
string diskPath = System.IO.Path.GetFullPath(assetPath);
|
|
18
|
+
diskSnapshot = System.IO.File.ReadAllBytes(diskPath);
|
|
19
|
+
var shader = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Shader>(assetPath);
|
|
20
|
+
if (shader == null) throw new System.Exception("shader not found: " + assetPath);
|
|
21
|
+
|
|
22
|
+
var publicInstance = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance;
|
|
23
|
+
var windowCreate = typeof(AmplifyShaderEditor.AmplifyShaderEditorWindow).GetMethod(
|
|
24
|
+
"CreateNode", publicInstance, null,
|
|
25
|
+
new System.Type[] { typeof(System.Type), typeof(UnityEngine.Vector2), typeof(AmplifyShaderFunction), typeof(bool) }, null);
|
|
26
|
+
var graphCreateConnection = typeof(AmplifyShaderEditor.ParentGraph).GetMethod(
|
|
27
|
+
"CreateConnection", publicInstance, null,
|
|
28
|
+
new System.Type[] { typeof(int), typeof(int), typeof(int), typeof(int), typeof(bool) }, null);
|
|
29
|
+
var graphDeleteConnection = typeof(AmplifyShaderEditor.ParentGraph).GetMethod(
|
|
30
|
+
"DeleteConnection", publicInstance, null,
|
|
31
|
+
new System.Type[] { typeof(bool), typeof(int), typeof(int), typeof(bool), typeof(bool), typeof(bool) }, null);
|
|
32
|
+
var saveMethod = typeof(AmplifyShaderEditor.AmplifyShaderEditorWindow).GetMethod(
|
|
33
|
+
"SaveToDisk", publicInstance, null, new System.Type[] { typeof(bool) }, null);
|
|
34
|
+
if (windowCreate == null || graphCreateConnection == null || graphDeleteConnection == null || saveMethod == null)
|
|
35
|
+
throw new System.Exception("required WireNode routing API is unavailable in this ASE version");
|
|
36
|
+
|
|
37
|
+
win = UnityEditor.EditorWindow.CreateInstance<AmplifyShaderEditor.AmplifyShaderEditorWindow>();
|
|
38
|
+
AmplifyShaderEditor.UIUtils.CurrentWindow = win;
|
|
39
|
+
var delayedLoad = typeof(AmplifyShaderEditor.AmplifyShaderEditorWindow).GetField(
|
|
40
|
+
"m_delayedLoadObject", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
41
|
+
if (delayedLoad == null) throw new System.Exception("ASE delayed-load field not found");
|
|
42
|
+
delayedLoad.SetValue(win, shader);
|
|
43
|
+
win.Show();
|
|
44
|
+
var layoutEvent = new UnityEngine.Event(); layoutEvent.type = UnityEngine.EventType.Layout; win.SendEvent(layoutEvent);
|
|
45
|
+
var repaintEvent = new UnityEngine.Event(); repaintEvent.type = UnityEngine.EventType.Repaint; win.SendEvent(repaintEvent);
|
|
46
|
+
var graph = win.CurrentGraph;
|
|
47
|
+
if (graph == null || graph.CurrentMasterNode == null)
|
|
48
|
+
throw new System.Exception("ASE graph did not load in GUI event");
|
|
49
|
+
|
|
50
|
+
var createdIds = new Newtonsoft.Json.Linq.JArray();
|
|
51
|
+
foreach (Newtonsoft.Json.Linq.JObject route in routes)
|
|
52
|
+
{
|
|
53
|
+
string action = (string)route["action"];
|
|
54
|
+
var logical = (Newtonsoft.Json.Linq.JObject)route["logical_wire"];
|
|
55
|
+
int sourceNodeId = (int)logical["from_node"];
|
|
56
|
+
int sourcePortId = (int)logical["from_port"];
|
|
57
|
+
int targetNodeId = (int)logical["to_node"];
|
|
58
|
+
int targetPortId = (int)logical["to_port"];
|
|
59
|
+
var sourceNode = graph.GetNode(sourceNodeId);
|
|
60
|
+
var targetNode = graph.GetNode(targetNodeId);
|
|
61
|
+
if (sourceNode == null || targetNode == null ||
|
|
62
|
+
sourceNode.GetOutputPortByUniqueId(sourcePortId) == null ||
|
|
63
|
+
targetNode.GetInputPortByUniqueId(targetPortId) == null)
|
|
64
|
+
throw new System.Exception("WireNode route endpoint not found");
|
|
65
|
+
|
|
66
|
+
var anchors = (Newtonsoft.Json.Linq.JArray)route["anchors"];
|
|
67
|
+
if (action == "move")
|
|
68
|
+
{
|
|
69
|
+
foreach (Newtonsoft.Json.Linq.JObject anchor in anchors)
|
|
70
|
+
{
|
|
71
|
+
int wireNodeId = (int)anchor["wire_node_id"];
|
|
72
|
+
var node = graph.GetNode(wireNodeId);
|
|
73
|
+
if (node == null || node.GetType() != typeof(AmplifyShaderEditor.WireNode))
|
|
74
|
+
throw new System.Exception("existing WireNode route anchor not found");
|
|
75
|
+
var point = (Newtonsoft.Json.Linq.JArray)anchor["to"];
|
|
76
|
+
node.Vec2Position = new UnityEngine.Vector2((float)point[0], (float)point[1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (action == "add")
|
|
80
|
+
{
|
|
81
|
+
var targetPort = targetNode.GetInputPortByUniqueId(targetPortId);
|
|
82
|
+
if (!targetPort.IsConnectedTo(sourceNodeId, sourcePortId))
|
|
83
|
+
throw new System.Exception("logical wire is no longer a direct connection");
|
|
84
|
+
graphDeleteConnection.Invoke(
|
|
85
|
+
graph, new object[] { true, targetNodeId, targetPortId, false, true, false });
|
|
86
|
+
int previousNodeId = sourceNodeId;
|
|
87
|
+
int previousPortId = sourcePortId;
|
|
88
|
+
foreach (Newtonsoft.Json.Linq.JArray point in anchors)
|
|
89
|
+
{
|
|
90
|
+
var created = (AmplifyShaderEditor.ParentNode)windowCreate.Invoke(
|
|
91
|
+
win, new object[] {
|
|
92
|
+
typeof(AmplifyShaderEditor.WireNode),
|
|
93
|
+
new UnityEngine.Vector2((float)point[0], (float)point[1]), null, false
|
|
94
|
+
});
|
|
95
|
+
if (created == null || created.GetType() != typeof(AmplifyShaderEditor.WireNode))
|
|
96
|
+
throw new System.Exception("ASE failed to create WireNode");
|
|
97
|
+
graphCreateConnection.Invoke(
|
|
98
|
+
graph, new object[] {
|
|
99
|
+
created.UniqueId, created.InputPorts[0].PortId,
|
|
100
|
+
previousNodeId, previousPortId, false
|
|
101
|
+
});
|
|
102
|
+
if (!created.InputPorts[0].IsConnectedTo(previousNodeId, previousPortId))
|
|
103
|
+
throw new System.Exception("ASE rejected source-to-WireNode connection");
|
|
104
|
+
previousNodeId = created.UniqueId;
|
|
105
|
+
previousPortId = created.OutputPorts[0].PortId;
|
|
106
|
+
createdIds.Add(created.UniqueId);
|
|
107
|
+
}
|
|
108
|
+
graphCreateConnection.Invoke(
|
|
109
|
+
graph, new object[] { targetNodeId, targetPortId, previousNodeId, previousPortId, false });
|
|
110
|
+
if (!targetPort.IsConnectedTo(previousNodeId, previousPortId))
|
|
111
|
+
throw new System.Exception("ASE rejected WireNode-to-target connection");
|
|
112
|
+
}
|
|
113
|
+
else throw new System.Exception("unknown WireNode route action");
|
|
114
|
+
}
|
|
115
|
+
saved = (bool)saveMethod.Invoke(win, new object[] { false });
|
|
116
|
+
if (!saved) throw new System.Exception("ASE SaveToDisk returned false");
|
|
117
|
+
var response = new Newtonsoft.Json.Linq.JObject();
|
|
118
|
+
response["protocol"] = "ASECLI_WIRE_ROUTE_V1";
|
|
119
|
+
response["asset_path"] = assetPath;
|
|
120
|
+
response["ase_version"] = AmplifyShaderEditor.VersionInfo.StaticToString();
|
|
121
|
+
response["saved"] = true;
|
|
122
|
+
response["applied_routes"] = routes.Count;
|
|
123
|
+
response["created_node_ids"] = createdIds;
|
|
124
|
+
return "ASECLI_WIRE_ROUTE_V1:" + response.ToString(Newtonsoft.Json.Formatting.None);
|
|
125
|
+
}
|
|
126
|
+
catch
|
|
127
|
+
{
|
|
128
|
+
if (diskSnapshot != null)
|
|
129
|
+
{
|
|
130
|
+
System.IO.File.WriteAllBytes(System.IO.Path.GetFullPath(assetPath), diskSnapshot);
|
|
131
|
+
UnityEditor.AssetDatabase.ImportAsset(assetPath, UnityEditor.ImportAssetOptions.ForceUpdate);
|
|
132
|
+
}
|
|
133
|
+
throw;
|
|
134
|
+
}
|
|
135
|
+
finally
|
|
136
|
+
{
|
|
137
|
+
UnityEditor.Selection.activeObject = previousSelection;
|
|
138
|
+
AmplifyShaderEditor.UIUtils.CurrentWindow = previousWindow;
|
|
139
|
+
if (win != null) { win.Close(); UnityEngine.Object.DestroyImmediate(win); }
|
|
140
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Apply a fixed WireNode routing plan through the currently installed ASE API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .mcp_client import McpClient, McpError
|
|
10
|
+
from .editor_create import _execute_code_result_text
|
|
11
|
+
from .recompile import _detect_project_root
|
|
12
|
+
from .resource_text import compose_resource_text
|
|
13
|
+
|
|
14
|
+
WIRE_ROUTE_SNIPPET = compose_resource_text(
|
|
15
|
+
"asecli.bridge", "resources", ("wire_route.transaction.cs.txt",),
|
|
16
|
+
)
|
|
17
|
+
_RESULT_MARKER = "ASECLI_WIRE_ROUTE_V1:"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def apply_wire_routes_via_mcp(
|
|
21
|
+
shader_path: str | Path,
|
|
22
|
+
changes: tuple[dict, ...],
|
|
23
|
+
*,
|
|
24
|
+
mcp_url: str = "http://127.0.0.1:8080/mcp",
|
|
25
|
+
instance_token: str | None = None,
|
|
26
|
+
unity_instance: str | None = None,
|
|
27
|
+
allow_remote_mcp: bool = False,
|
|
28
|
+
) -> dict:
|
|
29
|
+
target = Path(shader_path).resolve()
|
|
30
|
+
if not target.is_file():
|
|
31
|
+
raise FileNotFoundError(shader_path)
|
|
32
|
+
root = _detect_project_root(target)
|
|
33
|
+
asset_path = target.as_posix().removeprefix(root.as_posix() + "/")
|
|
34
|
+
routes = _validated_routes(changes)
|
|
35
|
+
payload = {"version": 1, "asset_path": asset_path, "routes": routes}
|
|
36
|
+
encoded = base64.b64encode(
|
|
37
|
+
json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
38
|
+
).decode("ascii")
|
|
39
|
+
code = WIRE_ROUTE_SNIPPET.replace("{payload_base64}", encoded)
|
|
40
|
+
client = McpClient(mcp_url, instance_token=instance_token, allow_remote=allow_remote_mcp)
|
|
41
|
+
client.connect()
|
|
42
|
+
arguments = {"action": "execute", "code": code}
|
|
43
|
+
if unity_instance is not None:
|
|
44
|
+
arguments["unity_instance"] = unity_instance
|
|
45
|
+
result = client.call_tool("execute_code", arguments)
|
|
46
|
+
response = _parse_result(_execute_code_result_text(result, instance_token))
|
|
47
|
+
if response.get("protocol") != "ASECLI_WIRE_ROUTE_V1":
|
|
48
|
+
raise McpError("WireNode route protocol version is missing or unsupported")
|
|
49
|
+
if response.get("asset_path") != asset_path:
|
|
50
|
+
raise McpError("WireNode route returned a different asset path")
|
|
51
|
+
if response.get("saved") is not True:
|
|
52
|
+
raise McpError("WireNode route did not confirm saved=True")
|
|
53
|
+
if response.get("applied_routes") != len(routes):
|
|
54
|
+
raise McpError("WireNode route applied-count does not match the plan")
|
|
55
|
+
created = response.get("created_node_ids")
|
|
56
|
+
expected_created = sum(len(item["anchors"]) for item in routes if item["action"] == "add")
|
|
57
|
+
if not isinstance(created, list) or len(created) != expected_created:
|
|
58
|
+
raise McpError("WireNode route returned an invalid created-node manifest")
|
|
59
|
+
return response
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validated_routes(changes: tuple[dict, ...]) -> list[dict]:
|
|
63
|
+
routes = []
|
|
64
|
+
seen = set()
|
|
65
|
+
for change in changes:
|
|
66
|
+
action = change.get("action")
|
|
67
|
+
logical = change.get("logical_wire")
|
|
68
|
+
if action not in {"move", "add"} or not isinstance(logical, dict):
|
|
69
|
+
raise ValueError("invalid WireNode route change")
|
|
70
|
+
normalized = {}
|
|
71
|
+
for source, target in (
|
|
72
|
+
("from_node", "from_node"), ("from_port", "from_port"),
|
|
73
|
+
("to_node", "to_node"), ("to_port", "to_port"),
|
|
74
|
+
):
|
|
75
|
+
value = str(logical.get(source, ""))
|
|
76
|
+
if not value.lstrip("-").isdigit():
|
|
77
|
+
raise ValueError(f"WireNode route {source} must be numeric")
|
|
78
|
+
normalized[target] = int(value)
|
|
79
|
+
key = tuple(normalized.values())
|
|
80
|
+
if key in seen:
|
|
81
|
+
raise ValueError("duplicate WireNode logical route")
|
|
82
|
+
seen.add(key)
|
|
83
|
+
if action == "add":
|
|
84
|
+
anchors = change.get("anchors")
|
|
85
|
+
if not isinstance(anchors, list) or not 1 <= len(anchors) <= 2:
|
|
86
|
+
raise ValueError("a logical wire may add one or two WireNode anchors")
|
|
87
|
+
normalized_anchors = [_point(item) for item in anchors]
|
|
88
|
+
else:
|
|
89
|
+
anchors = change.get("anchors")
|
|
90
|
+
if not isinstance(anchors, list) or not anchors:
|
|
91
|
+
raise ValueError("WireNode move requires at least one existing anchor")
|
|
92
|
+
normalized_anchors = []
|
|
93
|
+
ids = set()
|
|
94
|
+
for item in anchors:
|
|
95
|
+
node_id = str(item.get("wire_node_id", ""))
|
|
96
|
+
if not node_id.lstrip("-").isdigit() or node_id in ids:
|
|
97
|
+
raise ValueError("WireNode move ids must be unique numeric values")
|
|
98
|
+
ids.add(node_id)
|
|
99
|
+
normalized_anchors.append({"wire_node_id": int(node_id), "to": _point(item.get("to"))})
|
|
100
|
+
routes.append({"action": action, "logical_wire": normalized, "anchors": normalized_anchors})
|
|
101
|
+
return routes
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _point(value) -> list[float]:
|
|
105
|
+
if not isinstance(value, (list, tuple)) or len(value) != 2:
|
|
106
|
+
raise ValueError("WireNode anchor must be an x,y pair")
|
|
107
|
+
try:
|
|
108
|
+
point = [float(value[0]), float(value[1])]
|
|
109
|
+
except (TypeError, ValueError) as exc:
|
|
110
|
+
raise ValueError("WireNode anchor coordinates must be numeric") from exc
|
|
111
|
+
if not all(abs(item) <= 10_000_000 for item in point):
|
|
112
|
+
raise ValueError("WireNode anchor coordinate is outside the supported canvas range")
|
|
113
|
+
return point
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_result(text: str) -> dict:
|
|
117
|
+
marker = text.find(_RESULT_MARKER)
|
|
118
|
+
if marker < 0:
|
|
119
|
+
raise McpError("MCP tool result did not contain the WireNode route marker")
|
|
120
|
+
raw = text[marker + len(_RESULT_MARKER):].lstrip()
|
|
121
|
+
try:
|
|
122
|
+
value, _ = json.JSONDecoder().raw_decode(raw)
|
|
123
|
+
except json.JSONDecodeError as exc:
|
|
124
|
+
raise McpError("MCP tool returned malformed WireNode route JSON") from exc
|
|
125
|
+
if not isinstance(value, dict):
|
|
126
|
+
raise McpError("MCP WireNode route result must be a JSON object")
|
|
127
|
+
return value
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Validation & checksum (TASK-0008/0009)."""
|
|
2
|
+
|
|
3
|
+
from .checksum import ChecksumFormatError, compute_checksum, fix_checksum, verify_checksum
|
|
4
|
+
from .usage import external_references_for_node, live_node_ids, usage_audit
|
|
5
|
+
from .validate import validate_file
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ChecksumFormatError", "compute_checksum", "external_references_for_node", "fix_checksum",
|
|
9
|
+
"live_node_ids", "usage_audit", "verify_checksum", "validate_file",
|
|
10
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""ASE checksum: SHA1 (uppercase hex) of the full text before ``//CHKSM=``.
|
|
2
|
+
|
|
3
|
+
Replicates ``IOUtils.CreateChecksum``; verified against real files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
MARKER = "//CHKSM="
|
|
12
|
+
_TRAILER = re.compile(r"(?m)^//CHKSM=([^\r\n]*)(\r?\n)?\Z")
|
|
13
|
+
_LINE_MARKER = re.compile(r"(?m)^//CHKSM=")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChecksumFormatError(ValueError):
|
|
17
|
+
"""Raised when checksum-like lines are ambiguous or not a trailer."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def compute_checksum(text_before: str) -> str:
|
|
21
|
+
return hashlib.sha1(text_before.encode("utf-8")).hexdigest().upper()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def verify_checksum(text: str) -> tuple[bool, str | None, str | None]:
|
|
25
|
+
match = _checksum_trailer(text)
|
|
26
|
+
if match is None:
|
|
27
|
+
return False, None, None
|
|
28
|
+
idx = match.start()
|
|
29
|
+
stored = match.group(1).strip()
|
|
30
|
+
actual = compute_checksum(text[:idx])
|
|
31
|
+
return bool(re.fullmatch(r"[0-9A-Fa-f]{40}", stored)) and stored.upper() == actual, stored, actual
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fix_checksum(text: str) -> str:
|
|
35
|
+
match = _checksum_trailer(text)
|
|
36
|
+
if match is None:
|
|
37
|
+
sep = "" if text.endswith("\n") else "\n"
|
|
38
|
+
return text + sep + MARKER + compute_checksum(text)
|
|
39
|
+
idx = match.start()
|
|
40
|
+
trailing_eol = match.group(2) or ""
|
|
41
|
+
return text[:idx] + MARKER + compute_checksum(text[:idx]) + trailing_eol
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _checksum_trailer(text: str) -> re.Match[str] | None:
|
|
45
|
+
markers = list(_LINE_MARKER.finditer(text))
|
|
46
|
+
match = _TRAILER.search(text)
|
|
47
|
+
if match is None:
|
|
48
|
+
if markers:
|
|
49
|
+
raise ChecksumFormatError("//CHKSM= must be the final standalone line")
|
|
50
|
+
return None
|
|
51
|
+
if len(markers) != 1 or markers[0].start() != match.start():
|
|
52
|
+
raise ChecksumFormatError("multiple //CHKSM= lines are ambiguous")
|
|
53
|
+
return match
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Structural checks for ASE Register/Get Local Var semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..core import (
|
|
6
|
+
AseGraph,
|
|
7
|
+
GET_LOCAL_VAR_TYPE,
|
|
8
|
+
REGISTER_LOCAL_VAR_TYPE,
|
|
9
|
+
parse_get_local_var,
|
|
10
|
+
parse_register_local_var,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def append_local_var_issues(graph: AseGraph, issues: list[dict]) -> None:
|
|
15
|
+
nodes_by_id = {node.node_id: node for node in graph.nodes}
|
|
16
|
+
semantic_edges: list[tuple[str, str]] = []
|
|
17
|
+
registers = {}
|
|
18
|
+
for node in graph.nodes:
|
|
19
|
+
if node.type_name != REGISTER_LOCAL_VAR_TYPE:
|
|
20
|
+
continue
|
|
21
|
+
try:
|
|
22
|
+
item = parse_register_local_var(node)
|
|
23
|
+
except ValueError as exc:
|
|
24
|
+
issues.append(_issue("MALFORMED_LOCAL_VAR", node.node_id, str(exc)))
|
|
25
|
+
continue
|
|
26
|
+
registers[node.node_id] = item
|
|
27
|
+
if not item["name"].strip():
|
|
28
|
+
issues.append(_issue(
|
|
29
|
+
"EMPTY_LOCAL_VAR_NAME", node.node_id,
|
|
30
|
+
f"RegisterLocalVarNode {node.node_id} has an empty name",
|
|
31
|
+
))
|
|
32
|
+
if not item["data_type"].strip():
|
|
33
|
+
issues.append(_issue(
|
|
34
|
+
"EMPTY_LOCAL_VAR_TYPE", node.node_id,
|
|
35
|
+
f"RegisterLocalVarNode {node.node_id} has an empty data type",
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
for node in graph.nodes:
|
|
39
|
+
if node.type_name != GET_LOCAL_VAR_TYPE:
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
item = parse_get_local_var(node)
|
|
43
|
+
except ValueError as exc:
|
|
44
|
+
issues.append(_issue("MALFORMED_LOCAL_VAR", node.node_id, str(exc)))
|
|
45
|
+
continue
|
|
46
|
+
target = nodes_by_id.get(item["register_id"])
|
|
47
|
+
if target is None:
|
|
48
|
+
issues.append(_issue(
|
|
49
|
+
"DANGLING_LOCAL_VAR", node.node_id,
|
|
50
|
+
f"GetLocalVarNode {node.node_id} references missing Register node {item['register_id']}",
|
|
51
|
+
))
|
|
52
|
+
continue
|
|
53
|
+
if target.type_name != REGISTER_LOCAL_VAR_TYPE:
|
|
54
|
+
issues.append(_issue(
|
|
55
|
+
"INVALID_LOCAL_VAR_TARGET", node.node_id,
|
|
56
|
+
f"GetLocalVarNode {node.node_id} references non-Register node {item['register_id']}",
|
|
57
|
+
))
|
|
58
|
+
continue
|
|
59
|
+
register = registers.get(item["register_id"])
|
|
60
|
+
if register is None:
|
|
61
|
+
continue
|
|
62
|
+
semantic_edges.append((item["register_id"], node.node_id))
|
|
63
|
+
if item["name"] != register["name"]:
|
|
64
|
+
issues.append(_issue(
|
|
65
|
+
"LOCAL_VAR_NAME_MISMATCH", node.node_id,
|
|
66
|
+
f"GetLocalVarNode {node.node_id} name {item['name']!r} does not match "
|
|
67
|
+
f"Register {item['register_id']} name {register['name']!r}",
|
|
68
|
+
))
|
|
69
|
+
if item["data_type"] != register["data_type"]:
|
|
70
|
+
issues.append(_issue(
|
|
71
|
+
"LOCAL_VAR_TYPE_MISMATCH", node.node_id,
|
|
72
|
+
f"GetLocalVarNode {node.node_id} type {item['data_type']!r} does not match "
|
|
73
|
+
f"Register {item['register_id']} type {register['data_type']!r}",
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
_append_cycle_issues(graph, semantic_edges, issues)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _issue(code: str, node_id: str, message: str) -> dict:
|
|
80
|
+
return {"code": code, "severity": "error", "node_id": node_id, "message": message}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _append_cycle_issues(graph: AseGraph, semantic_edges: list[tuple[str, str]], issues: list[dict]) -> None:
|
|
84
|
+
adjacency: dict[str, set[str]] = {node.node_id: set() for node in graph.nodes}
|
|
85
|
+
for wire in graph.wires:
|
|
86
|
+
if wire.out_node in adjacency and wire.in_node in adjacency:
|
|
87
|
+
adjacency[wire.out_node].add(wire.in_node)
|
|
88
|
+
for source, target in semantic_edges:
|
|
89
|
+
adjacency[source].add(target)
|
|
90
|
+
|
|
91
|
+
index = 0
|
|
92
|
+
indices: dict[str, int] = {}
|
|
93
|
+
lowlinks: dict[str, int] = {}
|
|
94
|
+
stack: list[str] = []
|
|
95
|
+
on_stack: set[str] = set()
|
|
96
|
+
components: list[set[str]] = []
|
|
97
|
+
|
|
98
|
+
def visit(node_id: str) -> None:
|
|
99
|
+
nonlocal index
|
|
100
|
+
indices[node_id] = lowlinks[node_id] = index
|
|
101
|
+
index += 1
|
|
102
|
+
stack.append(node_id)
|
|
103
|
+
on_stack.add(node_id)
|
|
104
|
+
for target in adjacency[node_id]:
|
|
105
|
+
if target not in indices:
|
|
106
|
+
visit(target)
|
|
107
|
+
lowlinks[node_id] = min(lowlinks[node_id], lowlinks[target])
|
|
108
|
+
elif target in on_stack:
|
|
109
|
+
lowlinks[node_id] = min(lowlinks[node_id], indices[target])
|
|
110
|
+
if lowlinks[node_id] == indices[node_id]:
|
|
111
|
+
component = set()
|
|
112
|
+
while True:
|
|
113
|
+
member = stack.pop()
|
|
114
|
+
on_stack.remove(member)
|
|
115
|
+
component.add(member)
|
|
116
|
+
if member == node_id:
|
|
117
|
+
break
|
|
118
|
+
components.append(component)
|
|
119
|
+
|
|
120
|
+
for node_id in adjacency:
|
|
121
|
+
if node_id not in indices:
|
|
122
|
+
visit(node_id)
|
|
123
|
+
|
|
124
|
+
semantic = set(semantic_edges)
|
|
125
|
+
for component in components:
|
|
126
|
+
cyclic = len(component) > 1 or any(node in adjacency[node] for node in component)
|
|
127
|
+
uses_local_var = any(source in component and target in component for source, target in semantic)
|
|
128
|
+
if cyclic and uses_local_var:
|
|
129
|
+
members = sorted(component, key=lambda value: (not value.lstrip("-").isdigit(), value))
|
|
130
|
+
issue = _issue(
|
|
131
|
+
"LOCAL_VAR_REFERENCE_CYCLE", members[0],
|
|
132
|
+
f"Local Var semantic references participate in a cycle: {', '.join(members)}",
|
|
133
|
+
)
|
|
134
|
+
issue["members"] = members
|
|
135
|
+
issues.append(issue)
|