asecli 0.8.3__tar.gz → 0.8.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. {asecli-0.8.3 → asecli-0.8.4}/PKG-INFO +7 -7
  2. {asecli-0.8.3 → asecli-0.8.4}/README.md +6 -6
  3. {asecli-0.8.3 → asecli-0.8.4}/pyproject.toml +1 -1
  4. {asecli-0.8.3 → asecli-0.8.4}/skills/asecli/SKILL.md +10 -0
  5. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/__init__.py +1 -1
  6. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/export_sg_command.py +34 -5
  7. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/conversion_report.py +3 -0
  8. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/decoder.py +2 -2
  9. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/evidence.py +45 -5
  10. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/model.py +2 -1
  11. asecli-0.8.4/src/asecli/sg_export/report_presentation.py +21 -0
  12. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/report_semantics.py +5 -2
  13. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/surface_matching.py +1 -35
  14. asecli-0.8.4/src/asecli/sg_export/surface_precision.py +66 -0
  15. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/surface_semantics.py +20 -22
  16. asecli-0.8.4/tests/test_export_precision_diagnostics.py +122 -0
  17. {asecli-0.8.3 → asecli-0.8.4}/tests/test_export_sg.py +7 -8
  18. asecli-0.8.4/tests/test_smoke.py +4 -0
  19. {asecli-0.8.3 → asecli-0.8.4}/tests/test_surface_semantics.py +14 -11
  20. {asecli-0.8.3 → asecli-0.8.4}/tests/test_surface_semantics_integration.py +2 -0
  21. {asecli-0.8.3 → asecli-0.8.4}/uv.lock +1 -1
  22. asecli-0.8.3/tests/test_smoke.py +0 -4
  23. {asecli-0.8.3 → asecli-0.8.4}/.github/workflows/ci.yml +0 -0
  24. {asecli-0.8.3 → asecli-0.8.4}/.github/workflows/publish.yml +0 -0
  25. {asecli-0.8.3 → asecli-0.8.4}/.gitignore +0 -0
  26. {asecli-0.8.3 → asecli-0.8.4}/.project-architect.json +0 -0
  27. {asecli-0.8.3 → asecli-0.8.4}/AGENTS.md +0 -0
  28. {asecli-0.8.3 → asecli-0.8.4}/LICENSE +0 -0
  29. {asecli-0.8.3 → asecli-0.8.4}/SECURITY.md +0 -0
  30. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/audit-supplement.json +0 -0
  31. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/change-register.md +0 -0
  32. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/delivery-protocol.md +0 -0
  33. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/project-charter.md +0 -0
  34. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/supply-chain-policy.md +0 -0
  35. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/timeline.md +0 -0
  36. {asecli-0.8.3 → asecli-0.8.4}/docs/00-governance/traceability.csv +0 -0
  37. {asecli-0.8.3 → asecli-0.8.4}/docs/01-architecture/assumption-experiments.md +0 -0
  38. {asecli-0.8.3 → asecli-0.8.4}/docs/01-architecture/module-map.md +0 -0
  39. {asecli-0.8.3 → asecli-0.8.4}/docs/01-architecture/project-architecture-and-requirements.md +0 -0
  40. {asecli-0.8.3 → asecli-0.8.4}/docs/01-architecture/technical-route.md +0 -0
  41. {asecli-0.8.3 → asecli-0.8.4}/docs/03-quality/bug-register.md +0 -0
  42. {asecli-0.8.3 → asecli-0.8.4}/docs/03-quality/regression-catalog.md +0 -0
  43. {asecli-0.8.3 → asecli-0.8.4}/docs/03-quality/test-strategy.md +0 -0
  44. {asecli-0.8.3 → asecli-0.8.4}/docs/04-delivery/project-plan-task-charter.md +0 -0
  45. {asecli-0.8.3 → asecli-0.8.4}/docs/04-delivery/roadmap.md +0 -0
  46. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-014104-audit-report.md +0 -0
  47. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-014104-optimization-plan.md +0 -0
  48. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-145343-audit-report.md +0 -0
  49. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-145343-optimization-plan.md +0 -0
  50. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-193521-audit-report.md +0 -0
  51. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-193521-optimization-plan.md +0 -0
  52. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-203438-audit-report.md +0 -0
  53. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-01-203438-optimization-plan.md +0 -0
  54. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-02-121155-audit-report.md +0 -0
  55. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-02-121155-optimization-plan.md +0 -0
  56. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-02-175509-audit-report.md +0 -0
  57. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-02-175509-optimization-plan.md +0 -0
  58. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-04-122304-audit-report.md +0 -0
  59. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-04-122304-optimization-plan.md +0 -0
  60. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-05-142617-audit-report.md +0 -0
  61. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-05-142617-optimization-plan.md +0 -0
  62. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-05-160945-audit-report.md +0 -0
  63. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-05-160945-optimization-plan.md +0 -0
  64. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-05-213523-audit-report.md +0 -0
  65. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-05-213523-optimization-plan.md +0 -0
  66. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-10-193631-audit-report.md +0 -0
  67. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-10-193631-optimization-plan.md +0 -0
  68. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-14-123640-audit-report.md +0 -0
  69. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-14-123640-optimization-plan.md +0 -0
  70. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-14-124947-audit-report.md +0 -0
  71. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-14-124947-optimization-plan.md +0 -0
  72. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-14-132814-audit-report.md +0 -0
  73. {asecli-0.8.3 → asecli-0.8.4}/docs/adversarial-audits/2026-09-14-132814-optimization-plan.md +0 -0
  74. {asecli-0.8.3 → asecli-0.8.4}/docs/first-principles/2026-08-31-231401-optimization-plan.md +0 -0
  75. {asecli-0.8.3 → asecli-0.8.4}/docs/first-principles/2026-09-01-190656-optimization-plan.md +0 -0
  76. {asecli-0.8.3 → asecli-0.8.4}/docs/proposals/2026-09-07-shadergraph-to-ase-requirements.md +0 -0
  77. {asecli-0.8.3 → asecli-0.8.4}/scripts/install.ps1 +0 -0
  78. {asecli-0.8.3 → asecli-0.8.4}/scripts/install.sh +0 -0
  79. {asecli-0.8.3 → asecli-0.8.4}/skills/asecli/references/conversion-publication-receipt.v1.schema.json +0 -0
  80. {asecli-0.8.3 → asecli-0.8.4}/skills/asecli/references/conversion-workflow.md +0 -0
  81. {asecli-0.8.3 → asecli-0.8.4}/skills/asecli/references/layout-standard.md +0 -0
  82. {asecli-0.8.3 → asecli-0.8.4}/skills/asecli/references/master-output-settings-standard.md +0 -0
  83. {asecli-0.8.3 → asecli-0.8.4}/skills/asecli/references/material-property-standard.md +0 -0
  84. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/__init__.py +0 -0
  85. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_editor_port_contract.py +0 -0
  86. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_editor_primitives.py +0 -0
  87. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_editor_spec_grammar.py +0 -0
  88. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_editor_spec_io.py +0 -0
  89. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_editor_spec_validation.py +0 -0
  90. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_gui_handoff_store.py +0 -0
  91. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_gui_project.py +0 -0
  92. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_gui_resource_store.py +0 -0
  93. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/_gui_resource_upgrade.py +0 -0
  94. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/editor_create.py +0 -0
  95. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/editor_spec.py +0 -0
  96. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/graph_geometry.py +0 -0
  97. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/graph_geometry_parser.py +0 -0
  98. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/graph_inspect.py +0 -0
  99. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/gui_handoff.py +0 -0
  100. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/gui_presentation.py +0 -0
  101. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/gui_provider_detection.py +0 -0
  102. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/gui_runtime_probe.py +0 -0
  103. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/gui_support.py +0 -0
  104. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/gui_support_resource.py +0 -0
  105. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/mcp_client.py +0 -0
  106. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/recompile.py +0 -0
  107. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/render_settings.py +0 -0
  108. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resource_text.py +0 -0
  109. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.authoring.part00.cs.txt +0 -0
  110. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.authoring.part01.cs.txt +0 -0
  111. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.authoring.store.cs.txt +0 -0
  112. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.condition.cs.txt +0 -0
  113. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.hydration.cs.txt +0 -0
  114. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.part00.cs.txt +0 -0
  115. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.part01.cs.txt +0 -0
  116. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.reconciliation.cs.txt +0 -0
  117. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/asecli_material_gui.transaction.cs.txt +0 -0
  118. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/editor_create.part00.cs.txt +0 -0
  119. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/editor_create.part01.cs.txt +0 -0
  120. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/editor_create.part02.cs.txt +0 -0
  121. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/editor_create.part03.cs.txt +0 -0
  122. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/editor_create.part04.cs.txt +0 -0
  123. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/resources/wire_route.transaction.cs.txt +0 -0
  124. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/bridge/wire_route.py +0 -0
  125. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/checks/__init__.py +0 -0
  126. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/checks/checksum.py +0 -0
  127. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/checks/local_vars.py +0 -0
  128. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/checks/usage.py +0 -0
  129. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/checks/validate.py +0 -0
  130. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/__init__.py +0 -0
  131. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/commands.py +0 -0
  132. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/commentary_command.py +0 -0
  133. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/contract.py +0 -0
  134. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/contract_command.py +0 -0
  135. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/conversion_output.py +0 -0
  136. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/create_command.py +0 -0
  137. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/custom_gui_command.py +0 -0
  138. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/graph_review_command.py +0 -0
  139. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/gui_provider.py +0 -0
  140. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/gui_support_command.py +0 -0
  141. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/io.py +0 -0
  142. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/layout_command.py +0 -0
  143. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/main.py +0 -0
  144. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/migrate_package_command.py +0 -0
  145. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/recompile_metadata.py +0 -0
  146. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/skill_command.py +0 -0
  147. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/cli/usage_command.py +0 -0
  148. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/contracts/editor-graph-spec.v3.schema.json +0 -0
  149. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/contracts/sgcli.native.v3.schema.json +0 -0
  150. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/contracts.py +0 -0
  151. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/__init__.py +0 -0
  152. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/comment_bounds.py +0 -0
  153. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/comment_layout.py +0 -0
  154. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/comment_metrics.py +0 -0
  155. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/comment_purpose.py +0 -0
  156. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/commentary.py +0 -0
  157. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/compiled_metadata.py +0 -0
  158. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/compiled_properties.py +0 -0
  159. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/custom_gui.py +0 -0
  160. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/custom_gui_versions.py +0 -0
  161. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/fishbone_placement.py +0 -0
  162. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/fishbone_topology.py +0 -0
  163. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/graph_ops.py +0 -0
  164. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/graph_review.py +0 -0
  165. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/island_layout.py +0 -0
  166. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/layout.py +0 -0
  167. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/layout_audit.py +0 -0
  168. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/layout_audit_geometry.py +0 -0
  169. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/layout_audit_repeated.py +0 -0
  170. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/layout_graph.py +0 -0
  171. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/local_vars.py +0 -0
  172. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/material_gui_condition.py +0 -0
  173. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/material_gui_protocol.py +0 -0
  174. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/material_gui_spec.py +0 -0
  175. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/meticulous_layout.py +0 -0
  176. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/model.py +0 -0
  177. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/property_presentation.py +0 -0
  178. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/reuse_plan.py +0 -0
  179. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/wire_geometry.py +0 -0
  180. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/core/wire_router.py +0 -0
  181. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/export_sg.py +0 -0
  182. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/schema/__init__.py +0 -0
  183. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/schema/data/observed.json +0 -0
  184. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/schema/data/schemas.json +0 -0
  185. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/schema_validation.py +0 -0
  186. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/__init__.py +0 -0
  187. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/alpha_modulate.py +0 -0
  188. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/binding.py +0 -0
  189. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/custom_expression.py +0 -0
  190. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/nodes.py +0 -0
  191. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/pass_blend.py +0 -0
  192. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/reconciliation.py +0 -0
  193. {asecli-0.8.3 → asecli-0.8.4}/src/asecli/sg_export/sources.py +0 -0
  194. {asecli-0.8.3 → asecli-0.8.4}/tests/TEST.md +0 -0
  195. {asecli-0.8.3 → asecli-0.8.4}/tests/__init__.py +0 -0
  196. {asecli-0.8.3 → asecli-0.8.4}/tests/conftest.py +0 -0
  197. {asecli-0.8.3 → asecli-0.8.4}/tests/editor_create_v3_fixture.py +0 -0
  198. {asecli-0.8.3 → asecli-0.8.4}/tests/fixtures/HLIT.shader +0 -0
  199. {asecli-0.8.3 → asecli-0.8.4}/tests/fixtures/asecli_material_gui_v0_2_0.cs.txt +0 -0
  200. {asecli-0.8.3 → asecli-0.8.4}/tests/fixtures/step-antialiasing.function.txt +0 -0
  201. {asecli-0.8.3 → asecli-0.8.4}/tests/test_adversarial_hardening.py +0 -0
  202. {asecli-0.8.3 → asecli-0.8.4}/tests/test_audit_fixes.py +0 -0
  203. {asecli-0.8.3 → asecli-0.8.4}/tests/test_bridge_contract.py +0 -0
  204. {asecli-0.8.3 → asecli-0.8.4}/tests/test_bridge_recompile.py +0 -0
  205. {asecli-0.8.3 → asecli-0.8.4}/tests/test_build_reproducibility.py +0 -0
  206. {asecli-0.8.3 → asecli-0.8.4}/tests/test_cli_contract.py +0 -0
  207. {asecli-0.8.3 → asecli-0.8.4}/tests/test_commentary.py +0 -0
  208. {asecli-0.8.3 → asecli-0.8.4}/tests/test_contract_schema.py +0 -0
  209. {asecli-0.8.3 → asecli-0.8.4}/tests/test_conversion_report_semantics.py +0 -0
  210. {asecli-0.8.3 → asecli-0.8.4}/tests/test_core_contract.py +0 -0
  211. {asecli-0.8.3 → asecli-0.8.4}/tests/test_create.py +0 -0
  212. {asecli-0.8.3 → asecli-0.8.4}/tests/test_create_constant_presentation.py +0 -0
  213. {asecli-0.8.3 → asecli-0.8.4}/tests/test_cross_cli_workflow.py +0 -0
  214. {asecli-0.8.3 → asecli-0.8.4}/tests/test_custom_gui.py +0 -0
  215. {asecli-0.8.3 → asecli-0.8.4}/tests/test_custom_gui_cli.py +0 -0
  216. {asecli-0.8.3 → asecli-0.8.4}/tests/test_editor_create_bridge.py +0 -0
  217. {asecli-0.8.3 → asecli-0.8.4}/tests/test_editor_create_cli.py +0 -0
  218. {asecli-0.8.3 → asecli-0.8.4}/tests/test_editor_create_e2e.py +0 -0
  219. {asecli-0.8.3 → asecli-0.8.4}/tests/test_editor_create_spec.py +0 -0
  220. {asecli-0.8.3 → asecli-0.8.4}/tests/test_editor_create_spec_v3.py +0 -0
  221. {asecli-0.8.3 → asecli-0.8.4}/tests/test_editor_render_settings.py +0 -0
  222. {asecli-0.8.3 → asecli-0.8.4}/tests/test_export_property_visibility.py +0 -0
  223. {asecli-0.8.3 → asecli-0.8.4}/tests/test_governance.py +0 -0
  224. {asecli-0.8.3 → asecli-0.8.4}/tests/test_graph_review.py +0 -0
  225. {asecli-0.8.3 → asecli-0.8.4}/tests/test_graph_safety.py +0 -0
  226. {asecli-0.8.3 → asecli-0.8.4}/tests/test_gui_handoff.py +0 -0
  227. {asecli-0.8.3 → asecli-0.8.4}/tests/test_gui_native_extension.py +0 -0
  228. {asecli-0.8.3 → asecli-0.8.4}/tests/test_gui_support.py +0 -0
  229. {asecli-0.8.3 → asecli-0.8.4}/tests/test_gui_support_cli_contract.py +0 -0
  230. {asecli-0.8.3 → asecli-0.8.4}/tests/test_gui_upgrade.py +0 -0
  231. {asecli-0.8.3 → asecli-0.8.4}/tests/test_install_scripts.py +0 -0
  232. {asecli-0.8.3 → asecli-0.8.4}/tests/test_island_layout.py +0 -0
  233. {asecli-0.8.3 → asecli-0.8.4}/tests/test_layout.py +0 -0
  234. {asecli-0.8.3 → asecli-0.8.4}/tests/test_layout_governance.py +0 -0
  235. {asecli-0.8.3 → asecli-0.8.4}/tests/test_layout_local_var_policy.py +0 -0
  236. {asecli-0.8.3 → asecli-0.8.4}/tests/test_material_gui_e2e.py +0 -0
  237. {asecli-0.8.3 → asecli-0.8.4}/tests/test_mcp_security.py +0 -0
  238. {asecli-0.8.3 → asecli-0.8.4}/tests/test_migrate_package.py +0 -0
  239. {asecli-0.8.3 → asecli-0.8.4}/tests/test_mutate.py +0 -0
  240. {asecli-0.8.3 → asecli-0.8.4}/tests/test_pass_blend.py +0 -0
  241. {asecli-0.8.3 → asecli-0.8.4}/tests/test_perf.py +0 -0
  242. {asecli-0.8.3 → asecli-0.8.4}/tests/test_property_presentation.py +0 -0
  243. {asecli-0.8.3 → asecli-0.8.4}/tests/test_roundtrip.py +0 -0
  244. {asecli-0.8.3 → asecli-0.8.4}/tests/test_schema_add.py +0 -0
  245. {asecli-0.8.3 → asecli-0.8.4}/tests/test_schema_samples.py +0 -0
  246. {asecli-0.8.3 → asecli-0.8.4}/tests/test_sg_export_sources.py +0 -0
  247. {asecli-0.8.3 → asecli-0.8.4}/tests/test_skill_install.py +0 -0
  248. {asecli-0.8.3 → asecli-0.8.4}/tests/test_supply_chain.py +0 -0
  249. {asecli-0.8.3 → asecli-0.8.4}/tests/test_usage_audit.py +0 -0
  250. {asecli-0.8.3 → asecli-0.8.4}/tests/test_wire_route_bridge.py +0 -0
  251. {asecli-0.8.3 → asecli-0.8.4}/tools/build_schema.py +0 -0
  252. {asecli-0.8.3 → asecli-0.8.4}/tools/check_ci_governance.py +0 -0
  253. {asecli-0.8.3 → asecli-0.8.4}/tools/check_document_governance.py +0 -0
  254. {asecli-0.8.3 → asecli-0.8.4}/tools/check_packed_executor_loc.py +0 -0
  255. {asecli-0.8.3 → asecli-0.8.4}/tools/check_regression_catalog.py +0 -0
  256. {asecli-0.8.3 → asecli-0.8.4}/tools/check_release_candidate.py +0 -0
  257. {asecli-0.8.3 → asecli-0.8.4}/tools/compare_build_artifacts.py +0 -0
  258. {asecli-0.8.3 → asecli-0.8.4}/tools/generate_sbom.py +0 -0
  259. {asecli-0.8.3 → asecli-0.8.4}/tools/independent_wheel_smoke.py +0 -0
  260. {asecli-0.8.3 → asecli-0.8.4}/tools/supply_chain_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: asecli
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Agent-native CLI for reading, modifying, validating and compiling Amplify Shader Editor assets
5
5
  Project-URL: Homepage, https://github.com/seeseeczl/ASECLI
6
6
  Project-URL: Repository, https://github.com/seeseeczl/ASECLI
@@ -28,7 +28,7 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  AseCLI 是面向 AI Agent 的本地 CLI:离线读取、编辑和校验 ASE 节点图;通过运行中的 Unity/团结引擎与 MCP,让真实 ASE 创建节点、提供精排几何并重新生成 HLSL。Skill 提供跨 Agent 的操作规则,CLI 提供可验证的执行结果。
30
30
 
31
- 当前正式版:**[0.8.3](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.3)** · [PyPI](https://pypi.org/project/asecli/0.8.3/) · [Agent 操作手册](skills/asecli/SKILL.md) · [SG 转换后整理流程](skills/asecli/references/conversion-workflow.md)
31
+ 当前正式版:**[0.8.4](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.4)** · [PyPI](https://pypi.org/project/asecli/0.8.4/) · [Agent 操作手册](skills/asecli/SKILL.md) · [SG 转换后整理流程](skills/asecli/references/conversion-workflow.md)
32
32
 
33
33
  ASECLI 接收 SGCLI 生成的裸 `EditorGraphSpec v3`,并可把受支持的 ASE Shader 直接导出为 SGCLI 的裸 `sgcli.native.v3`。两个方向都只有“生产者 spec 原文件→消费者 `--spec`”;独立 report 与发布 receipt 都不进入创建输入,创建成功也不代表转换效果等价。
34
34
 
@@ -132,7 +132,7 @@ curl -fsSL https://raw.githubusercontent.com/seeseeczl/ASECLI/main/scripts/insta
132
132
  uv tool uninstall asecli
133
133
  ```
134
134
 
135
- 当前正式版本为 `0.8.3`(独立 CI/发布流程与单工具 wheel 验证),可从 [PyPI](https://pypi.org/project/asecli/0.8.3/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.3) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.8.0` 保留为回滚点。
135
+ 当前正式版本为 `0.8.4`(ASE→SG 诊断收敛、Custom Function 原样证据与 MZGUI 呈现分层),可从 [PyPI](https://pypi.org/project/asecli/0.8.4/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.4) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.8.3` 保留为回滚点。
136
136
 
137
137
  已知限制:真实 MCP 下,属性+纹理 v2 和 Reciprocal 降级 v2 的 create、manifest、validate、独立 recompile 已通过;SGCLI SphereMask v3 spec 在重载时仍可能返回 `node missing after reload: Radius` 并回滚。本次不承诺该 recipe 端到端可用,也不改变 SGCLI v2 默认值契约。
138
138
 
@@ -162,7 +162,7 @@ asecli --help
162
162
 
163
163
  ```bash
164
164
  shasum -a 256 -c SHA256SUMS
165
- uv tool install /path/to/asecli-0.8.3-py3-none-any.whl
165
+ uv tool install /path/to/asecli-0.8.4-py3-none-any.whl
166
166
  asecli --help
167
167
  ```
168
168
 
@@ -340,9 +340,9 @@ asecli recompile Assets/Example.shader
340
340
  除显式 `--help` 保留人类可读文本外,stdout 恒为单行严格 JSON,agent 可直接解析。`--version` 也返回 JSON:
341
341
 
342
342
  ```json
343
- {"contract_version":1,"cli_version":"0.8.3","command":"parse","ok":true,"data":{"node_count":7}}
344
- {"contract_version":1,"cli_version":"0.8.3","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
345
- {"contract_version":1,"cli_version":"0.8.3","command":"version","ok":true,"data":{"version":"0.8.3"}}
343
+ {"contract_version":1,"cli_version":"0.8.4","command":"parse","ok":true,"data":{"node_count":7}}
344
+ {"contract_version":1,"cli_version":"0.8.4","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
345
+ {"contract_version":1,"cli_version":"0.8.4","command":"version","ok":true,"data":{"version":"0.8.4"}}
346
346
  ```
347
347
 
348
348
  常见错误码:`PARSE_ERROR`、`NOT_FOUND`、`USAGE_ERROR`、`SCHEMA_UNAVAILABLE`、`SCHEMA_VERSION_MISMATCH`、`VALIDATION_ERROR`、`PROPERTY_PRESENTATION_ERROR`、`LAYOUT_ERROR`、`CHECKSUM_FORMAT_ERROR`、`GUI_SUPPORT_ERROR`、`CUSTOM_GUI_ERROR`、`COMMENT_GROUP_ERROR`、`GRAPH_REVIEW_ERROR`、`SEMANTIC_MISMATCH`、`SG_EXPORT_BLOCKED`、`EXTERNAL_REFERENCE`、`SKILL_INSTALL_CONFLICT`、`SKILL_INSTALL_ERROR`、`WRITE_CONFLICT`、`WRITE_PARTIAL`、`UNSAFE_PATH`、`WRITE_ERROR`、`BRIDGE_ERROR`、`INTERNAL`。
@@ -4,7 +4,7 @@
4
4
 
5
5
  AseCLI 是面向 AI Agent 的本地 CLI:离线读取、编辑和校验 ASE 节点图;通过运行中的 Unity/团结引擎与 MCP,让真实 ASE 创建节点、提供精排几何并重新生成 HLSL。Skill 提供跨 Agent 的操作规则,CLI 提供可验证的执行结果。
6
6
 
7
- 当前正式版:**[0.8.3](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.3)** · [PyPI](https://pypi.org/project/asecli/0.8.3/) · [Agent 操作手册](skills/asecli/SKILL.md) · [SG 转换后整理流程](skills/asecli/references/conversion-workflow.md)
7
+ 当前正式版:**[0.8.4](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.4)** · [PyPI](https://pypi.org/project/asecli/0.8.4/) · [Agent 操作手册](skills/asecli/SKILL.md) · [SG 转换后整理流程](skills/asecli/references/conversion-workflow.md)
8
8
 
9
9
  ASECLI 接收 SGCLI 生成的裸 `EditorGraphSpec v3`,并可把受支持的 ASE Shader 直接导出为 SGCLI 的裸 `sgcli.native.v3`。两个方向都只有“生产者 spec 原文件→消费者 `--spec`”;独立 report 与发布 receipt 都不进入创建输入,创建成功也不代表转换效果等价。
10
10
 
@@ -108,7 +108,7 @@ curl -fsSL https://raw.githubusercontent.com/seeseeczl/ASECLI/main/scripts/insta
108
108
  uv tool uninstall asecli
109
109
  ```
110
110
 
111
- 当前正式版本为 `0.8.3`(独立 CI/发布流程与单工具 wheel 验证),可从 [PyPI](https://pypi.org/project/asecli/0.8.3/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.3) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.8.0` 保留为回滚点。
111
+ 当前正式版本为 `0.8.4`(ASE→SG 诊断收敛、Custom Function 原样证据与 MZGUI 呈现分层),可从 [PyPI](https://pypi.org/project/asecli/0.8.4/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.4) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.8.3` 保留为回滚点。
112
112
 
113
113
  已知限制:真实 MCP 下,属性+纹理 v2 和 Reciprocal 降级 v2 的 create、manifest、validate、独立 recompile 已通过;SGCLI SphereMask v3 spec 在重载时仍可能返回 `node missing after reload: Radius` 并回滚。本次不承诺该 recipe 端到端可用,也不改变 SGCLI v2 默认值契约。
114
114
 
@@ -138,7 +138,7 @@ asecli --help
138
138
 
139
139
  ```bash
140
140
  shasum -a 256 -c SHA256SUMS
141
- uv tool install /path/to/asecli-0.8.3-py3-none-any.whl
141
+ uv tool install /path/to/asecli-0.8.4-py3-none-any.whl
142
142
  asecli --help
143
143
  ```
144
144
 
@@ -316,9 +316,9 @@ asecli recompile Assets/Example.shader
316
316
  除显式 `--help` 保留人类可读文本外,stdout 恒为单行严格 JSON,agent 可直接解析。`--version` 也返回 JSON:
317
317
 
318
318
  ```json
319
- {"contract_version":1,"cli_version":"0.8.3","command":"parse","ok":true,"data":{"node_count":7}}
320
- {"contract_version":1,"cli_version":"0.8.3","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
321
- {"contract_version":1,"cli_version":"0.8.3","command":"version","ok":true,"data":{"version":"0.8.3"}}
319
+ {"contract_version":1,"cli_version":"0.8.4","command":"parse","ok":true,"data":{"node_count":7}}
320
+ {"contract_version":1,"cli_version":"0.8.4","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
321
+ {"contract_version":1,"cli_version":"0.8.4","command":"version","ok":true,"data":{"version":"0.8.4"}}
322
322
  ```
323
323
 
324
324
  常见错误码:`PARSE_ERROR`、`NOT_FOUND`、`USAGE_ERROR`、`SCHEMA_UNAVAILABLE`、`SCHEMA_VERSION_MISMATCH`、`VALIDATION_ERROR`、`PROPERTY_PRESENTATION_ERROR`、`LAYOUT_ERROR`、`CHECKSUM_FORMAT_ERROR`、`GUI_SUPPORT_ERROR`、`CUSTOM_GUI_ERROR`、`COMMENT_GROUP_ERROR`、`GRAPH_REVIEW_ERROR`、`SEMANTIC_MISMATCH`、`SG_EXPORT_BLOCKED`、`EXTERNAL_REFERENCE`、`SKILL_INSTALL_CONFLICT`、`SKILL_INSTALL_ERROR`、`WRITE_CONFLICT`、`WRITE_PARTIAL`、`UNSAFE_PATH`、`WRITE_ERROR`、`BRIDGE_ERROR`、`INTERNAL`。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asecli"
3
- version = "0.8.3"
3
+ version = "0.8.4"
4
4
  description = "Agent-native CLI for reading, modifying, validating and compiling Amplify Shader Editor assets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -22,6 +22,16 @@ description: Create, modify, validate, and strictly lay out Amplify Shader Edito
22
22
  - 旧 `{graph,report}` wrapper 只经 `asecli migrate-package OLD --out-dir DIR --extract-sg-spec` 按登记形状显式提取;只接受内层 `sgcli.native.v3`,未知或 v2 wrapper 失败关闭。无版本 discriminator 时记录 `legacy_format`,不能声称识别出 legacy schema 版本。
23
23
  - 报告中的 `source_snapshot_sha256/source_recheck_sha256` 必须一致;`producer_schema_validated` 与 `consumer_loaded` 分开记录,未实际调用 SGCLI 时后者保持 `not_run`。
24
24
 
25
+ ### 导出阻断的确定性处理
26
+
27
+ - 先核对 CLI 路径/版本和源 SHA;当前 `.shader`、历史 `.bak`、旧 report 不可混为同一次转换证据。
28
+ - 新版失败返回的顶层 `data` 包含 `blockers`、`blocker_count`、`unverified_checks`、源快照与 `surface_semantics`。它是本次检测清单,不保证穷尽所有潜在问题;旧版没有这些字段时读取 `report_json`,不要猜测只有一个阻断。
29
+ - `ALPHA_MODULATE_TARGET_CAPABILITY_REQUIRED` 表示末端表达式不能安全折叠到 URP 的 half AlphaModulate;读取 `required_target_capabilities`,不得把 `MultiplySourceAlpha` 用于需要保留背景 Alpha 的源 Shader。
30
+ - 上游 Custom Function 作为目标 Custom Function 原样交接;用 `custom_function_manifest` 核对节点、精度和规范化函数载荷 SHA,不因 Agent 无法解释函数数学意义而拒绝。
31
+ - 已确认的 `MZGUI.MZGUI`、Tooltip/Foldout/HelpBox 只影响 Inspector 呈现,列在 `presentation_warnings`,不阻断渲染语义交接。其他未知 Custom ShaderGUI 仍可能有材质副作用并失败关闭。
32
+ - `retry_unchanged_input=false` 时不要重复相同导出、猜命令或找强制开关。报告缺失能力并停止;SGCLI 的新增能力必须通过公开 JSON 契约另行交接,不跨仓修补。
33
+ - 原生 `.shadergraph` 虽然使用 JSON 序列化,但不是 `sgcli.native.v3` 创建 spec。只有本次命令成功并核对 spec/report/receipt 完整提交与 SHA,才能称“导出成功”;这仍不等于 Editor 编译或最终效果一致已验收。
34
+
25
35
  ## Agent 兼容性
26
36
 
27
37
  本 Skill 遵循通用 Agent Skills 目录结构:标准 YAML frontmatter、`SKILL.md` 正文和相对路径引用。它只要求 Agent 能运行本地 shell 命令并读取 ASECLI 的单行 JSON,不依赖 Codex、Claude Code、Cursor、Gemini CLI 或 GitHub Copilot 的专属工具名、权限模型或消息格式。
@@ -1,3 +1,3 @@
1
1
  """AseCLI — agent-native toolkit for Amplify Shader Editor assets."""
2
2
 
3
- __version__ = "0.8.3"
3
+ __version__ = "0.8.4"
@@ -106,24 +106,24 @@ def cmd_export_sg(args) -> dict:
106
106
  raise ValueError(f'resource changed before publication: {file}')
107
107
  except ExportError as exc:
108
108
  internal = exc.report
109
- _write_failure_report(
109
+ failure_report = _write_failure_report(
110
110
  source, spec_path, report_path, internal, str(exc),
111
111
  source_raw=source_raw, schema_status=schema_status,
112
112
  )
113
113
  raise CliError(
114
114
  "SG_EXPORT_BLOCKED",
115
115
  str(exc),
116
- {"report_json": str(report_path)},
116
+ _failure_details(report_path, internal, failure_report),
117
117
  ) from exc
118
118
  except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
119
- _write_failure_report(
119
+ failure_report = _write_failure_report(
120
120
  source, spec_path, report_path, internal, str(exc),
121
121
  source_raw=source_raw, schema_status=schema_status,
122
122
  )
123
123
  raise CliError(
124
124
  "SG_EXPORT_BLOCKED",
125
125
  str(exc),
126
- {"report_json": str(report_path)},
126
+ _failure_details(report_path, internal, failure_report),
127
127
  ) from exc
128
128
  write_result = _write_pair(spec_path, spec, report_path, report)
129
129
  return {
@@ -175,7 +175,7 @@ def _write_failure_report(
175
175
  *,
176
176
  source_raw: bytes,
177
177
  schema_status: str,
178
- ) -> None:
178
+ ) -> dict:
179
179
  if report_path.exists():
180
180
  raise CliError("WRITE_CONFLICT", f"output already exists: {report_path}")
181
181
  report = build_public_report(
@@ -189,6 +189,35 @@ def _write_failure_report(
189
189
  producer_schema_validated=schema_status,
190
190
  )
191
191
  _write_new(report_path, report)
192
+ return report
193
+
194
+
195
+ def _failure_details(report_path: Path, internal: dict | None, report: dict) -> dict:
196
+ """Agent-facing diagnostics, separate from the fixed consumer report schema."""
197
+ internal = internal or {}
198
+ blockers = [dict(item, classification="semantic") for item in internal.get("diagnostics", [])]
199
+ blockers.extend(dict(item) for item in internal.get("degradations", []))
200
+ warnings = [dict(item) for item in internal.get("presentation_warnings", [])]
201
+ return {
202
+ "report_json": str(report_path),
203
+ "spec_written": False,
204
+ "receipt_written": False,
205
+ "source": report["source"],
206
+ "blockers": blockers,
207
+ "blocker_count": len(blockers),
208
+ "blocker_scope": "detected_in_this_run_not_exhaustive",
209
+ "presentation_warnings": warnings,
210
+ "warning_count": len(warnings),
211
+ "custom_function_manifest": internal.get("custom_function_manifest", []),
212
+ "required_target_capabilities": (internal.get("surface_semantics") or {}).get(
213
+ "required_target_capabilities"
214
+ ),
215
+ "unverified_checks": [name for name, check in report["checks"].items()
216
+ if check["status"] != "verified"],
217
+ "surface_semantics": internal.get("surface_semantics"),
218
+ "retry_unchanged_input": False,
219
+ "next_action": "Inspect all blockers and report evidence; do not force export, substitute a .shadergraph, or edit the source to bypass validation",
220
+ }
192
221
 
193
222
 
194
223
  def _assert_source_unchanged(source: Path, snapshot: bytes, phase: str) -> bytes:
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import Any
9
9
 
10
10
  from .report_semantics import check_evidence
11
+ from .report_presentation import presentation_degradations
11
12
 
12
13
 
13
14
  CHECK_RULES = {
@@ -76,6 +77,7 @@ def build_public_report(
76
77
  "blend_equation": "verified" if blend_passed else "unknown",
77
78
  }
78
79
  internal_degradations = (internal or {}).get('degradations', [])
80
+ presentation_warnings = (internal or {}).get('presentation_warnings', [])
79
81
  if internal_degradations:
80
82
  check_status['properties'] = 'unsupported'
81
83
  checks = {
@@ -125,6 +127,7 @@ def build_public_report(
125
127
  + str(item.get('impact', 'Inspector behavior is unproven'))
126
128
  + '; details=' + json.dumps(item.get('details', []), ensure_ascii=False),
127
129
  })
130
+ degradations.extend(presentation_degradations(presentation_warnings, source, spec_path))
128
131
  if failure:
129
132
  degradations.append({
130
133
  "rule": "SEM-REPORT-001",
@@ -13,13 +13,12 @@ from .model import (
13
13
  )
14
14
  from .nodes import convert_node
15
15
  from .sources import compiled_properties, load_asset_map
16
- from .evidence import inspector_degradations, record_port, validate_template_passes
16
+ from .evidence import inspector_degradations, record_custom_functions, record_port, validate_template_passes
17
17
  from .surface_semantics import normalize_surface_outputs
18
18
  from .reconciliation import (
19
19
  groups, reconcile_shaderlab_properties, unique_properties, verify_texture_dependencies,
20
20
  )
21
21
 
22
-
23
22
  def build_candidate(
24
23
  source_path,
25
24
  target_project,
@@ -124,6 +123,7 @@ def build_candidate(
124
123
  for node in nodes:
125
124
  for target_id in node.source_ids:
126
125
  edges.append(SemanticEdge(node.target_id, 0, target_id, -1))
126
+ record_custom_functions(nodes, report)
127
127
  nodes, edges = normalize_surface_outputs(nodes, edges, target_spec, mappings, report, text)
128
128
  groups_spec = groups(ase, mappings, diagnostics)
129
129
  report["output_bindings"] = [
@@ -3,10 +3,29 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
+ import hashlib
7
+ import json
6
8
 
9
+ from .alpha_modulate import _strip_comments
7
10
  from .model import MASTER_TYPE, URP_UNLIT_GUID, URP_UNLIT_PASSES, diagnostic
8
11
 
9
12
 
13
+ def record_custom_functions(nodes, report):
14
+ manifest = []
15
+ for node in nodes:
16
+ if node.target_type != "custom-function" or node.function is None:
17
+ continue
18
+ canonical = json.dumps(node.function, ensure_ascii=False, sort_keys=True,
19
+ separators=(",", ":")).encode("utf-8")
20
+ manifest.append({
21
+ "source_node_id": node.source_id, "target_node_id": node.target_id,
22
+ "function_sha256": hashlib.sha256(canonical).hexdigest(),
23
+ "precision": node.settings.get("precision", "Inherit"),
24
+ "mapping": "preserved_custom_function_payload",
25
+ })
26
+ report["custom_function_manifest"] = manifest
27
+
28
+
10
29
  def validate_template_passes(ase, master, diagnostics, report):
11
30
  masters = [node for node in ase.graph.nodes if node.type_name == MASTER_TYPE]
12
31
  passes = {node.raw_fields[15] for node in masters if len(node.raw_fields) > 15}
@@ -38,18 +57,39 @@ def inspector_degradations(ase, properties, report):
38
57
  match = re.search(rf"(?m)^\s*(?P<attrs>(?:\[[^\]\r\n]*\]\s*)*){re.escape(name)}\s*\(", ase.prefix)
39
58
  present = [kind for kind in attributes if match and f"[{kind}" in match.group("attrs")]
40
59
  if present:
41
- report["degradations"].append({
60
+ report["presentation_warnings"].append({
42
61
  "source": {"property": name}, "code": "INSPECTOR_METADATA_NOT_MIGRATED",
43
62
  "details": present, "impact": "Property data is preserved; ASE Inspector UI behavior is not migrated",
63
+ "classification": "presentation_only",
64
+ "source_attributes": match.group("attrs").strip(),
65
+ "material_side_effects": "none",
44
66
  })
45
- custom_gui = sorted({node.raw_fields[9] for node in ase.graph.nodes
67
+ graph_gui = {node.raw_fields[9] for node in ase.graph.nodes
46
68
  if node.type_name == MASTER_TYPE and len(node.raw_fields) > 9
47
69
  and node.raw_fields[6] == "True"
48
- and node.raw_fields[9] not in {"", "UnityEditor.ShaderGraphUnlitGUI"}})
49
- if custom_gui:
70
+ and node.raw_fields[9] not in {"", "UnityEditor.ShaderGraphUnlitGUI"}}
71
+ # Compiled ShaderLab may disagree with the ASE graph. Neither declaration
72
+ # can be ignored when an unknown GUI can alter keywords/material state.
73
+ shaderlab = ase.prefix.rsplit("/*ASEBEGIN", 1)[0]
74
+ compiled_gui = set(re.findall(r'\bCustomEditor\s+"([^"]+)"', _strip_comments(shaderlab) or ""))
75
+ custom_gui = (graph_gui | compiled_gui) - {"", "UnityEditor.ShaderGraphUnlitGUI"}
76
+ known_presentation = sorted(custom_gui & {"MZGUI.MZGUI"})
77
+ unknown_gui = sorted(custom_gui - {"MZGUI.MZGUI"})
78
+ if known_presentation:
79
+ report["presentation_warnings"].append({
80
+ "source": {"file": str(report["source"]["path"])},
81
+ "code": "CUSTOM_INSPECTOR_PRESENTATION_NOT_MIGRATED",
82
+ "details": known_presentation,
83
+ "classification": "presentation_only",
84
+ "material_side_effects": "none",
85
+ "impact": "MZGUI Inspector styling is not migrated; Shader properties and runtime graph semantics are unchanged",
86
+ })
87
+ if unknown_gui:
50
88
  report["degradations"].append({
51
89
  "source": {"file": str(report["source"]["path"])}, "code": "CUSTOM_INSPECTOR_NOT_MIGRATED",
52
- "details": custom_gui,
90
+ "details": unknown_gui,
91
+ "classification": "custom_shader_gui",
92
+ "material_side_effects": "unproven",
53
93
  "impact": "ShaderGUI behavior has no equivalent in the creation specification",
54
94
  })
55
95
  if report["degradations"]:
@@ -78,7 +78,8 @@ def report_template(source: Path, target: Path, version: str, source_raw: bytes)
78
78
  "mapping_version": MAPPING_VERSION,
79
79
  "receiver_schema": {"name": "sgcli.native.v3", "path": None, "sha256": None},
80
80
  "source_validation": {"error_count": 0, "warning_count": 0, "issues": []},
81
- "mappings": [], "dependencies": [], "degradations": [], "diagnostics": [],
81
+ "mappings": [], "dependencies": [], "degradations": [], "presentation_warnings": [],
82
+ "custom_function_manifest": [], "diagnostics": [],
82
83
  "equivalence": {"status": "not_proven", "reason": "effect validation has not run"},
83
84
  "verification": {
84
85
  "source_parse": dict(empty), "source_structure": dict(empty),
@@ -0,0 +1,21 @@
1
+ """Map non-rendering Inspector differences into public evidence gaps."""
2
+
3
+ import json
4
+
5
+
6
+ def presentation_degradations(items, source, target):
7
+ result = []
8
+ for item in items:
9
+ location = item.get("source") or {}
10
+ if isinstance(location, dict):
11
+ location = str(location.get("file") or source) + (
12
+ "#" + str(location["property"]) if "property" in location else ""
13
+ )
14
+ result.append({
15
+ "rule": "SEM-PRESENTATION-001", "category": "evidence_gap",
16
+ "source": str(location or source), "target": str(target),
17
+ "reason": str(item.get("code", "PRESENTATION_NOT_MIGRATED")) + ": "
18
+ + str(item.get("impact", "Inspector presentation is not migrated"))
19
+ + "; details=" + json.dumps(item.get("details", []), ensure_ascii=False),
20
+ })
21
+ return result
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
+ import json
6
7
 
7
8
 
8
9
  _VERIFIED_EVIDENCE = {
@@ -17,6 +18,7 @@ _VERIFIED_EVIDENCE = {
17
18
  _SEMANTIC_CODES = {
18
19
  "ALPHA_BLEND_UNREPRESENTABLE", "PASS_BLEND_UNPROVEN",
19
20
  "DUPLICATE_ALPHA_MODULATE_UNRESOLVED", "ALPHA_MODULATE_UNPROVEN",
21
+ "ALPHA_MODULATE_TARGET_CAPABILITY_REQUIRED",
20
22
  }
21
23
 
22
24
 
@@ -28,9 +30,9 @@ def check_evidence(
28
30
  return _VERIFIED_EVIDENCE[name] + details
29
31
  diagnostics = internal.get("diagnostics", []) if isinstance(internal, dict) else []
30
32
  codes = _SEMANTIC_CODES if name in {"alpha_equation", "blend_equation"} else set()
31
- relevant = next((item for item in diagnostics if item.get("code") in codes), None)
33
+ relevant = [item for item in diagnostics if item.get("code") in codes]
32
34
  if relevant:
33
- return f"{relevant['code']}: {relevant.get('reason', 'not proven')}" + details
35
+ return json.dumps(relevant, ensure_ascii=False, sort_keys=True) + details
34
36
  if failure:
35
37
  return f"conversion stopped before this invariant was proved: {failure}" + details
36
38
  if diagnostics:
@@ -49,6 +51,7 @@ def _blend_details(name: str, internal: dict[str, Any] | None) -> str:
49
51
  rewrites = semantics.get("rewrites") or []
50
52
  matcher = (
51
53
  "unresolved" if semantics.get("unresolved")
54
+ or semantics.get("fold_precision", {}).get("status") == "unproven"
52
55
  else "folded" if rewrites
53
56
  else "preserved_graph_float_expression"
54
57
  if semantics.get("alpha_modulate_strategy") == "preserve_graph_float_expression"
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from .alpha_modulate import inputs as alpha_modulate_inputs
6
5
  from .model import SemanticEdge, SemanticNode, diagnostic
7
6
 
8
7
 
@@ -39,7 +38,7 @@ def _unproven_modulation(report: dict, semantics: dict, node: SemanticNode) -> N
39
38
  report.setdefault("diagnostics", []).append(diagnostic(
40
39
  node.source_id,
41
40
  "ALPHA_MODULATE_UNPROVEN",
42
- reason,
41
+ {"message": reason, "function": node.function, "precision": node.settings.get("precision", "Inherit")},
43
42
  "The exporter cannot prove that target AlphaModulate will execute exactly once",
44
43
  ))
45
44
 
@@ -134,36 +133,3 @@ def _white_value(value) -> bool:
134
133
 
135
134
  def _one(value) -> bool:
136
135
  return isinstance(value, (int, float)) and not isinstance(value, bool) and float(value) == 1.0
137
-
138
-
139
- def _first_upstream_custom_function(
140
- nodes: list[SemanticNode], edges: list[SemanticEdge], source_id: str
141
- ) -> SemanticNode | None:
142
- by_id = {node.target_id: node for node in nodes}
143
- incoming: dict[str, list[str]] = {}
144
- for edge in edges:
145
- incoming.setdefault(edge.target_id, []).append(edge.source_id)
146
- pending = [source_id]
147
- visited: set[str] = set()
148
- while pending:
149
- current = pending.pop()
150
- if current in visited:
151
- continue
152
- visited.add(current)
153
- node = by_id.get(current)
154
- if node is not None and node.target_type == "custom-function":
155
- return node
156
- pending.extend(reversed(incoming.get(current, [])))
157
- return None
158
-
159
-
160
- def _reject_upstream_function(report: dict, semantics: dict, node: SemanticNode) -> None:
161
- if alpha_modulate_inputs(node.function) is not None:
162
- _unresolved_modulation(
163
- report,
164
- semantics,
165
- node,
166
- "AlphaModulate remains upstream of the Multiply BaseColor output",
167
- )
168
- else:
169
- _unproven_modulation(report, semantics, node)
@@ -0,0 +1,66 @@
1
+ """Conservative precision proof for moving graph arithmetic into URP."""
2
+
3
+ import re
4
+
5
+ from .alpha_modulate import _strip_comments
6
+ from .model import diagnostic
7
+
8
+
9
+ def fold_precision_verified(node, nodes, edges, report, semantics):
10
+ """Do not replace float/inherited lerp with URP's half3/half overload.
11
+
12
+ Custom Function parameters establish their own half conversion boundary.
13
+ Native lerp additionally needs explicitly half operands (no mixed promotion).
14
+ This is a narrow proof, not an assertion that half equals float on every GPU.
15
+ """
16
+ precision = node.settings.get("precision", "Inherit")
17
+ effective_precision = precision
18
+ verified = precision == "Half"
19
+ body = None
20
+ if node.target_type == "custom-function":
21
+ body = _strip_comments((node.function or {}).get("body", ""))
22
+ if body and re.search(r"\bfloat(?:[1-4])?\s*\(", body):
23
+ effective_precision = "Single"
24
+ elif body and re.search(r"\bhalf(?:[1-4])?\s*\(", body):
25
+ effective_precision = "Half"
26
+ verified = verified and body is not None and bool(re.search(r"\bhalf3\s*\(", body))
27
+ else:
28
+ by_id = {item.target_id: item for item in nodes}
29
+ operands = [by_id.get(edge.source_id) for edge in edges if edge.target_id == node.target_id]
30
+ verified = verified and bool(operands) and all(
31
+ item is not None and item.settings.get("precision") == "Half" for item in operands
32
+ )
33
+ proof = {
34
+ "source_node_id": node.source_id,
35
+ "source_node_precision": precision,
36
+ "source_effective_precision": effective_precision,
37
+ "source_expression": body,
38
+ "target_precision": "Half",
39
+ "target_function": "half3 AlphaModulate(half3 albedo, half alpha)",
40
+ "status": "verified" if verified else "unproven",
41
+ }
42
+ semantics["fold_precision"] = proof
43
+ if not verified:
44
+ requirement = {
45
+ "surface": semantics.get("source", {}).get("surface"),
46
+ "rgb_blend": (semantics.get("source_pass_blend") or {}).get("rgb"),
47
+ "alpha_blend": (semantics.get("source_pass_blend") or {}).get("alpha"),
48
+ "explicit_alpha_modulate": {
49
+ "preserve_in_graph": True,
50
+ "precision": effective_precision,
51
+ "source_node_id": node.source_id,
52
+ },
53
+ "implicit_alpha_modulate": False,
54
+ }
55
+ semantics["required_target_capabilities"] = requirement
56
+ semantics["unresolved"] = {
57
+ "rule": "SEM-BLEND-001",
58
+ "source_node": node.source_id,
59
+ "reason": "target cannot preserve explicit AlphaModulate precision while disabling its implicit AlphaModulate",
60
+ }
61
+ report.setdefault("diagnostics", []).append(diagnostic(
62
+ node.source_id, "ALPHA_MODULATE_TARGET_CAPABILITY_REQUIRED",
63
+ {"precision_proof": proof, "required_target_capabilities": requirement},
64
+ "The current public SG target contract cannot preserve this graph expression and blend equation without adding a second half-precision AlphaModulate",
65
+ ))
66
+ return verified
@@ -12,13 +12,13 @@ from __future__ import annotations
12
12
  from .alpha_modulate import inputs as alpha_modulate_inputs
13
13
  from .model import SemanticEdge, SemanticNode, diagnostic
14
14
  from .pass_blend import record_pass_blend
15
+ from .surface_precision import fold_precision_verified
15
16
  from .surface_matching import (
16
- _first_upstream_custom_function,
17
17
  _input_port,
18
18
  _native_alpha_modulate,
19
- _reject_upstream_function,
20
19
  _single_edge_to,
21
20
  _target_alpha_modulates,
21
+ _through_reroutes,
22
22
  _unproven_modulation,
23
23
  _unresolved_modulation,
24
24
  )
@@ -95,9 +95,8 @@ def normalize_surface_outputs(
95
95
  native = _native_alpha_modulate(nodes, edges, color_edge, alpha_edge)
96
96
  if native is not None:
97
97
  modulate, color_input, alpha_input, route_nodes, white_node = native
98
- upstream = _first_upstream_custom_function(nodes, edges, color_input.source_id)
99
- if upstream is not None:
100
- _reject_upstream_function(report, semantics, upstream)
98
+ precision_ok = fold_precision_verified(modulate, nodes, edges, report, semantics)
99
+ if not precision_ok:
101
100
  return nodes, edges
102
101
  replacement = SemanticEdge(
103
102
  color_input.source_id,
@@ -152,6 +151,10 @@ def normalize_surface_outputs(
152
151
  row["target_nodes"] = [] if removed else row.get("target_nodes", [])
153
152
  row["rule"] = "SEM-BLEND-001"
154
153
  return nodes, rewritten
154
+ source_edge, route_nodes = _through_reroutes(color_edge, by_id, edges)
155
+ node = by_id.get(source_edge.source_id)
156
+ if node is None:
157
+ return nodes, edges
155
158
  matched = (
156
159
  alpha_modulate_inputs(node.function)
157
160
  if node.target_type == "custom-function"
@@ -160,10 +163,6 @@ def normalize_surface_outputs(
160
163
  if matched is None:
161
164
  if node.target_type == "custom-function":
162
165
  _unproven_modulation(report, semantics, node)
163
- else:
164
- upstream = _first_upstream_custom_function(nodes, edges, node.target_id)
165
- if upstream is not None:
166
- _reject_upstream_function(report, semantics, upstream)
167
166
  return nodes, edges
168
167
  color_input_name, alpha_input_name = matched
169
168
  color_port = _input_port(node, color_input_name)
@@ -186,9 +185,8 @@ def normalize_surface_outputs(
186
185
  "AlphaModulate alpha input differs from Surface Alpha",
187
186
  )
188
187
  return nodes, edges
189
- upstream = _first_upstream_custom_function(nodes, edges, color_input.source_id)
190
- if upstream is not None:
191
- _reject_upstream_function(report, semantics, upstream)
188
+ precision_ok = fold_precision_verified(node, nodes, edges, report, semantics)
189
+ if not precision_ok:
192
190
  return nodes, edges
193
191
  replacement = SemanticEdge(
194
192
  color_input.source_id,
@@ -197,16 +195,16 @@ def normalize_surface_outputs(
197
195
  color_edge.target_port,
198
196
  )
199
197
  rewritten = [replacement if edge == color_edge else edge for edge in edges]
200
- remaining_consumers = [
201
- edge for edge in rewritten
202
- if edge.source_id == node.target_id
203
- ]
204
- removed = False
205
- if not remaining_consumers:
206
- rewritten = [edge for edge in rewritten if edge.target_id != node.target_id]
207
- nodes = [item for item in nodes if item.target_id != node.target_id]
208
- mappings.pop(node.source_id, None)
209
- removed = True
198
+ removable_ids = {node.target_id, *(item.target_id for item in route_nodes)}
199
+ external_consumers = [edge for edge in rewritten
200
+ if edge.source_id in removable_ids and edge.target_id not in removable_ids]
201
+ removed = not external_consumers
202
+ if removed:
203
+ rewritten = [edge for edge in rewritten
204
+ if edge.source_id not in removable_ids and edge.target_id not in removable_ids]
205
+ nodes = [item for item in nodes if item.target_id not in removable_ids]
206
+ for item in [node, *route_nodes]:
207
+ mappings.pop(item.source_id, None)
210
208
 
211
209
  semantics["source"].update({
212
210
  "explicit_alpha_modulate": True,