asecli 0.8.0__tar.gz → 0.8.1__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 (250) hide show
  1. {asecli-0.8.0 → asecli-0.8.1}/PKG-INFO +8 -8
  2. {asecli-0.8.0 → asecli-0.8.1}/README.md +7 -7
  3. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/change-register.md +1 -1
  4. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/timeline.md +2 -1
  5. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/traceability.csv +1 -1
  6. {asecli-0.8.0 → asecli-0.8.1}/docs/04-delivery/project-plan-task-charter.md +2 -1
  7. {asecli-0.8.0 → asecli-0.8.1}/pyproject.toml +1 -1
  8. {asecli-0.8.0 → asecli-0.8.1}/scripts/install.ps1 +1 -1
  9. {asecli-0.8.0 → asecli-0.8.1}/scripts/install.sh +1 -1
  10. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/__init__.py +1 -1
  11. asecli-0.8.1/src/asecli/sg_export/alpha_modulate.py +113 -0
  12. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/conversion_report.py +18 -28
  13. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/decoder.py +3 -3
  14. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/model.py +1 -1
  15. asecli-0.8.1/src/asecli/sg_export/pass_blend.py +200 -0
  16. asecli-0.8.1/src/asecli/sg_export/report_semantics.py +57 -0
  17. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/sources.py +31 -15
  18. asecli-0.8.1/src/asecli/sg_export/surface_matching.py +165 -0
  19. asecli-0.8.1/src/asecli/sg_export/surface_semantics.py +228 -0
  20. asecli-0.8.1/tests/test_conversion_report_semantics.py +73 -0
  21. {asecli-0.8.0 → asecli-0.8.1}/tests/test_install_scripts.py +2 -2
  22. asecli-0.8.1/tests/test_pass_blend.py +69 -0
  23. asecli-0.8.1/tests/test_sg_export_sources.py +56 -0
  24. asecli-0.8.1/tests/test_smoke.py +4 -0
  25. asecli-0.8.1/tests/test_surface_semantics.py +331 -0
  26. asecli-0.8.1/tests/test_surface_semantics_integration.py +45 -0
  27. {asecli-0.8.0 → asecli-0.8.1}/uv.lock +1 -1
  28. asecli-0.8.0/tests/test_smoke.py +0 -4
  29. {asecli-0.8.0 → asecli-0.8.1}/.github/workflows/ci.yml +0 -0
  30. {asecli-0.8.0 → asecli-0.8.1}/.github/workflows/cross-cli.yml +0 -0
  31. {asecli-0.8.0 → asecli-0.8.1}/.github/workflows/publish.yml +0 -0
  32. {asecli-0.8.0 → asecli-0.8.1}/.gitignore +0 -0
  33. {asecli-0.8.0 → asecli-0.8.1}/.project-architect.json +0 -0
  34. {asecli-0.8.0 → asecli-0.8.1}/AGENTS.md +0 -0
  35. {asecli-0.8.0 → asecli-0.8.1}/LICENSE +0 -0
  36. {asecli-0.8.0 → asecli-0.8.1}/SECURITY.md +0 -0
  37. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/audit-supplement.json +0 -0
  38. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/delivery-protocol.md +0 -0
  39. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/project-charter.md +0 -0
  40. {asecli-0.8.0 → asecli-0.8.1}/docs/00-governance/supply-chain-policy.md +0 -0
  41. {asecli-0.8.0 → asecli-0.8.1}/docs/01-architecture/assumption-experiments.md +0 -0
  42. {asecli-0.8.0 → asecli-0.8.1}/docs/01-architecture/module-map.md +0 -0
  43. {asecli-0.8.0 → asecli-0.8.1}/docs/01-architecture/project-architecture-and-requirements.md +0 -0
  44. {asecli-0.8.0 → asecli-0.8.1}/docs/01-architecture/technical-route.md +0 -0
  45. {asecli-0.8.0 → asecli-0.8.1}/docs/03-quality/bug-register.md +0 -0
  46. {asecli-0.8.0 → asecli-0.8.1}/docs/03-quality/regression-catalog.md +0 -0
  47. {asecli-0.8.0 → asecli-0.8.1}/docs/03-quality/test-strategy.md +0 -0
  48. {asecli-0.8.0 → asecli-0.8.1}/docs/04-delivery/roadmap.md +0 -0
  49. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-014104-audit-report.md +0 -0
  50. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-014104-optimization-plan.md +0 -0
  51. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-145343-audit-report.md +0 -0
  52. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-145343-optimization-plan.md +0 -0
  53. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-193521-audit-report.md +0 -0
  54. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-193521-optimization-plan.md +0 -0
  55. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-203438-audit-report.md +0 -0
  56. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-01-203438-optimization-plan.md +0 -0
  57. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-02-121155-audit-report.md +0 -0
  58. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-02-121155-optimization-plan.md +0 -0
  59. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-02-175509-audit-report.md +0 -0
  60. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-02-175509-optimization-plan.md +0 -0
  61. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-04-122304-audit-report.md +0 -0
  62. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-04-122304-optimization-plan.md +0 -0
  63. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-05-142617-audit-report.md +0 -0
  64. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-05-142617-optimization-plan.md +0 -0
  65. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-05-160945-audit-report.md +0 -0
  66. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-05-160945-optimization-plan.md +0 -0
  67. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-05-213523-audit-report.md +0 -0
  68. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-05-213523-optimization-plan.md +0 -0
  69. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-10-193631-audit-report.md +0 -0
  70. {asecli-0.8.0 → asecli-0.8.1}/docs/adversarial-audits/2026-09-10-193631-optimization-plan.md +0 -0
  71. {asecli-0.8.0 → asecli-0.8.1}/docs/first-principles/2026-08-31-231401-optimization-plan.md +0 -0
  72. {asecli-0.8.0 → asecli-0.8.1}/docs/first-principles/2026-09-01-190656-optimization-plan.md +0 -0
  73. {asecli-0.8.0 → asecli-0.8.1}/docs/proposals/2026-09-07-shadergraph-to-ase-requirements.md +0 -0
  74. {asecli-0.8.0 → asecli-0.8.1}/skills/asecli/SKILL.md +0 -0
  75. {asecli-0.8.0 → asecli-0.8.1}/skills/asecli/references/conversion-publication-receipt.v1.schema.json +0 -0
  76. {asecli-0.8.0 → asecli-0.8.1}/skills/asecli/references/conversion-workflow.md +0 -0
  77. {asecli-0.8.0 → asecli-0.8.1}/skills/asecli/references/layout-standard.md +0 -0
  78. {asecli-0.8.0 → asecli-0.8.1}/skills/asecli/references/master-output-settings-standard.md +0 -0
  79. {asecli-0.8.0 → asecli-0.8.1}/skills/asecli/references/material-property-standard.md +0 -0
  80. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/__init__.py +0 -0
  81. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_editor_port_contract.py +0 -0
  82. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_editor_primitives.py +0 -0
  83. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_editor_spec_grammar.py +0 -0
  84. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_editor_spec_io.py +0 -0
  85. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_editor_spec_validation.py +0 -0
  86. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_gui_handoff_store.py +0 -0
  87. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_gui_project.py +0 -0
  88. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_gui_resource_store.py +0 -0
  89. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/_gui_resource_upgrade.py +0 -0
  90. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/editor_create.py +0 -0
  91. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/editor_spec.py +0 -0
  92. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/graph_geometry.py +0 -0
  93. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/graph_geometry_parser.py +0 -0
  94. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/graph_inspect.py +0 -0
  95. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/gui_handoff.py +0 -0
  96. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/gui_presentation.py +0 -0
  97. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/gui_provider_detection.py +0 -0
  98. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/gui_runtime_probe.py +0 -0
  99. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/gui_support.py +0 -0
  100. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/gui_support_resource.py +0 -0
  101. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/mcp_client.py +0 -0
  102. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/recompile.py +0 -0
  103. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/render_settings.py +0 -0
  104. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resource_text.py +0 -0
  105. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.authoring.part00.cs.txt +0 -0
  106. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.authoring.part01.cs.txt +0 -0
  107. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.authoring.store.cs.txt +0 -0
  108. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.condition.cs.txt +0 -0
  109. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.hydration.cs.txt +0 -0
  110. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.part00.cs.txt +0 -0
  111. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.part01.cs.txt +0 -0
  112. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.reconciliation.cs.txt +0 -0
  113. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/asecli_material_gui.transaction.cs.txt +0 -0
  114. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/editor_create.part00.cs.txt +0 -0
  115. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/editor_create.part01.cs.txt +0 -0
  116. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/editor_create.part02.cs.txt +0 -0
  117. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/editor_create.part03.cs.txt +0 -0
  118. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/editor_create.part04.cs.txt +0 -0
  119. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/resources/wire_route.transaction.cs.txt +0 -0
  120. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/bridge/wire_route.py +0 -0
  121. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/checks/__init__.py +0 -0
  122. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/checks/checksum.py +0 -0
  123. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/checks/local_vars.py +0 -0
  124. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/checks/usage.py +0 -0
  125. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/checks/validate.py +0 -0
  126. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/__init__.py +0 -0
  127. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/commands.py +0 -0
  128. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/commentary_command.py +0 -0
  129. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/contract.py +0 -0
  130. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/contract_command.py +0 -0
  131. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/conversion_output.py +0 -0
  132. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/create_command.py +0 -0
  133. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/custom_gui_command.py +0 -0
  134. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/export_sg_command.py +0 -0
  135. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/graph_review_command.py +0 -0
  136. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/gui_provider.py +0 -0
  137. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/gui_support_command.py +0 -0
  138. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/io.py +0 -0
  139. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/layout_command.py +0 -0
  140. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/main.py +0 -0
  141. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/migrate_package_command.py +0 -0
  142. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/recompile_metadata.py +0 -0
  143. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/skill_command.py +0 -0
  144. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/cli/usage_command.py +0 -0
  145. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/contracts/editor-graph-spec.v3.schema.json +0 -0
  146. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/contracts/sgcli.native.v3.schema.json +0 -0
  147. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/contracts.py +0 -0
  148. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/__init__.py +0 -0
  149. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/comment_bounds.py +0 -0
  150. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/comment_layout.py +0 -0
  151. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/comment_metrics.py +0 -0
  152. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/comment_purpose.py +0 -0
  153. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/commentary.py +0 -0
  154. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/compiled_metadata.py +0 -0
  155. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/compiled_properties.py +0 -0
  156. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/custom_gui.py +0 -0
  157. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/custom_gui_versions.py +0 -0
  158. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/fishbone_placement.py +0 -0
  159. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/fishbone_topology.py +0 -0
  160. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/graph_ops.py +0 -0
  161. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/graph_review.py +0 -0
  162. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/island_layout.py +0 -0
  163. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/layout.py +0 -0
  164. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/layout_audit.py +0 -0
  165. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/layout_audit_geometry.py +0 -0
  166. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/layout_audit_repeated.py +0 -0
  167. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/layout_graph.py +0 -0
  168. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/local_vars.py +0 -0
  169. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/material_gui_condition.py +0 -0
  170. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/material_gui_protocol.py +0 -0
  171. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/material_gui_spec.py +0 -0
  172. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/meticulous_layout.py +0 -0
  173. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/model.py +0 -0
  174. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/property_presentation.py +0 -0
  175. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/reuse_plan.py +0 -0
  176. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/wire_geometry.py +0 -0
  177. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/core/wire_router.py +0 -0
  178. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/export_sg.py +0 -0
  179. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/schema/__init__.py +0 -0
  180. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/schema/data/observed.json +0 -0
  181. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/schema/data/schemas.json +0 -0
  182. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/schema_validation.py +0 -0
  183. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/__init__.py +0 -0
  184. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/binding.py +0 -0
  185. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/custom_expression.py +0 -0
  186. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/evidence.py +0 -0
  187. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/nodes.py +0 -0
  188. {asecli-0.8.0 → asecli-0.8.1}/src/asecli/sg_export/reconciliation.py +0 -0
  189. {asecli-0.8.0 → asecli-0.8.1}/tests/TEST.md +0 -0
  190. {asecli-0.8.0 → asecli-0.8.1}/tests/__init__.py +0 -0
  191. {asecli-0.8.0 → asecli-0.8.1}/tests/conftest.py +0 -0
  192. {asecli-0.8.0 → asecli-0.8.1}/tests/editor_create_v3_fixture.py +0 -0
  193. {asecli-0.8.0 → asecli-0.8.1}/tests/fixtures/HLIT.shader +0 -0
  194. {asecli-0.8.0 → asecli-0.8.1}/tests/fixtures/asecli_material_gui_v0_2_0.cs.txt +0 -0
  195. {asecli-0.8.0 → asecli-0.8.1}/tests/fixtures/step-antialiasing.function.txt +0 -0
  196. {asecli-0.8.0 → asecli-0.8.1}/tests/test_adversarial_hardening.py +0 -0
  197. {asecli-0.8.0 → asecli-0.8.1}/tests/test_audit_fixes.py +0 -0
  198. {asecli-0.8.0 → asecli-0.8.1}/tests/test_bridge_contract.py +0 -0
  199. {asecli-0.8.0 → asecli-0.8.1}/tests/test_bridge_recompile.py +0 -0
  200. {asecli-0.8.0 → asecli-0.8.1}/tests/test_build_reproducibility.py +0 -0
  201. {asecli-0.8.0 → asecli-0.8.1}/tests/test_cli_contract.py +0 -0
  202. {asecli-0.8.0 → asecli-0.8.1}/tests/test_commentary.py +0 -0
  203. {asecli-0.8.0 → asecli-0.8.1}/tests/test_contract_schema.py +0 -0
  204. {asecli-0.8.0 → asecli-0.8.1}/tests/test_core_contract.py +0 -0
  205. {asecli-0.8.0 → asecli-0.8.1}/tests/test_create.py +0 -0
  206. {asecli-0.8.0 → asecli-0.8.1}/tests/test_create_constant_presentation.py +0 -0
  207. {asecli-0.8.0 → asecli-0.8.1}/tests/test_cross_cli_workflow.py +0 -0
  208. {asecli-0.8.0 → asecli-0.8.1}/tests/test_custom_gui.py +0 -0
  209. {asecli-0.8.0 → asecli-0.8.1}/tests/test_custom_gui_cli.py +0 -0
  210. {asecli-0.8.0 → asecli-0.8.1}/tests/test_editor_create_bridge.py +0 -0
  211. {asecli-0.8.0 → asecli-0.8.1}/tests/test_editor_create_cli.py +0 -0
  212. {asecli-0.8.0 → asecli-0.8.1}/tests/test_editor_create_e2e.py +0 -0
  213. {asecli-0.8.0 → asecli-0.8.1}/tests/test_editor_create_spec.py +0 -0
  214. {asecli-0.8.0 → asecli-0.8.1}/tests/test_editor_create_spec_v3.py +0 -0
  215. {asecli-0.8.0 → asecli-0.8.1}/tests/test_editor_render_settings.py +0 -0
  216. {asecli-0.8.0 → asecli-0.8.1}/tests/test_export_sg.py +0 -0
  217. {asecli-0.8.0 → asecli-0.8.1}/tests/test_governance.py +0 -0
  218. {asecli-0.8.0 → asecli-0.8.1}/tests/test_graph_review.py +0 -0
  219. {asecli-0.8.0 → asecli-0.8.1}/tests/test_graph_safety.py +0 -0
  220. {asecli-0.8.0 → asecli-0.8.1}/tests/test_gui_handoff.py +0 -0
  221. {asecli-0.8.0 → asecli-0.8.1}/tests/test_gui_native_extension.py +0 -0
  222. {asecli-0.8.0 → asecli-0.8.1}/tests/test_gui_support.py +0 -0
  223. {asecli-0.8.0 → asecli-0.8.1}/tests/test_gui_support_cli_contract.py +0 -0
  224. {asecli-0.8.0 → asecli-0.8.1}/tests/test_gui_upgrade.py +0 -0
  225. {asecli-0.8.0 → asecli-0.8.1}/tests/test_island_layout.py +0 -0
  226. {asecli-0.8.0 → asecli-0.8.1}/tests/test_layout.py +0 -0
  227. {asecli-0.8.0 → asecli-0.8.1}/tests/test_layout_governance.py +0 -0
  228. {asecli-0.8.0 → asecli-0.8.1}/tests/test_layout_local_var_policy.py +0 -0
  229. {asecli-0.8.0 → asecli-0.8.1}/tests/test_material_gui_e2e.py +0 -0
  230. {asecli-0.8.0 → asecli-0.8.1}/tests/test_mcp_security.py +0 -0
  231. {asecli-0.8.0 → asecli-0.8.1}/tests/test_migrate_package.py +0 -0
  232. {asecli-0.8.0 → asecli-0.8.1}/tests/test_mutate.py +0 -0
  233. {asecli-0.8.0 → asecli-0.8.1}/tests/test_perf.py +0 -0
  234. {asecli-0.8.0 → asecli-0.8.1}/tests/test_property_presentation.py +0 -0
  235. {asecli-0.8.0 → asecli-0.8.1}/tests/test_roundtrip.py +0 -0
  236. {asecli-0.8.0 → asecli-0.8.1}/tests/test_schema_add.py +0 -0
  237. {asecli-0.8.0 → asecli-0.8.1}/tests/test_schema_samples.py +0 -0
  238. {asecli-0.8.0 → asecli-0.8.1}/tests/test_skill_install.py +0 -0
  239. {asecli-0.8.0 → asecli-0.8.1}/tests/test_supply_chain.py +0 -0
  240. {asecli-0.8.0 → asecli-0.8.1}/tests/test_usage_audit.py +0 -0
  241. {asecli-0.8.0 → asecli-0.8.1}/tests/test_wire_route_bridge.py +0 -0
  242. {asecli-0.8.0 → asecli-0.8.1}/tools/build_schema.py +0 -0
  243. {asecli-0.8.0 → asecli-0.8.1}/tools/check_ci_governance.py +0 -0
  244. {asecli-0.8.0 → asecli-0.8.1}/tools/check_document_governance.py +0 -0
  245. {asecli-0.8.0 → asecli-0.8.1}/tools/check_packed_executor_loc.py +0 -0
  246. {asecli-0.8.0 → asecli-0.8.1}/tools/check_regression_catalog.py +0 -0
  247. {asecli-0.8.0 → asecli-0.8.1}/tools/check_release_candidate.py +0 -0
  248. {asecli-0.8.0 → asecli-0.8.1}/tools/compare_build_artifacts.py +0 -0
  249. {asecli-0.8.0 → asecli-0.8.1}/tools/generate_sbom.py +0 -0
  250. {asecli-0.8.0 → asecli-0.8.1}/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.0
3
+ Version: 0.8.1
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.0](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.0)** · [PyPI](https://pypi.org/project/asecli/0.8.0/) · [Agent 操作手册](skills/asecli/SKILL.md) · [SG 转换后整理流程](skills/asecli/references/conversion-workflow.md)
31
+ 当前正式版:**[0.8.1](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.1)** · [PyPI](https://pypi.org/project/asecli/0.8.1/) · [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.0`(双向消费者原生裸 JSON 规格直连与可审计发布证据),可从 [PyPI](https://pypi.org/project/asecli/0.8.0/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.0) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.7.0` 保留为回滚点。
135
+ 当前正式版本为 `0.8.1`(透明 Multiply 的 matcher-v4 语义修复与转换认证加固),可从 [PyPI](https://pypi.org/project/asecli/0.8.1/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.1) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.8.0` 保留为回滚点。
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.0-py3-none-any.whl
165
+ uv tool install /path/to/asecli-0.8.1-py3-none-any.whl
166
166
  asecli --help
167
167
  ```
168
168
 
@@ -207,7 +207,7 @@ sgcli sg create Assets/ConvertedNew.shadergraph \
207
207
 
208
208
  `export-sg` 自动从源 Shader 向上识别 Unity/Tuanjie 工程;跨工程资源解析时显式传 `--target-project`。它强制生成 `<stem>.asecli-to-sgcli.spec.json`、`.report.json` 与 `.receipt.json`,同名即失败;receipt 只证明 spec/report 两份文件的提交状态与 SHA,不是中间交接包。命令不调用 SGCLI,也不生成 ASE 自有中间 JSON。未知节点、Master/Pass、外部属性或纹理 Importer 语义时只写规范报告,不写半成品 spec。反方向 `.asecli-to-sgcli.spec.json` 不能交给 ASECLI `create --spec`。
209
209
 
210
- ASECLI 随 wheel 发布自己拥有的 EditorGraphSpec v3 Schema,并固定一份 SGCLI `0.3.0` 的 `sgcli.native.v3` 生产快照;SGCLI 同样固定 ASECLI `0.8.0` 快照。双仓兼容检查会比较两组原始 Schema SHA-256,任何漂移都必须先升级生产者再放行。
210
+ ASECLI 随 wheel 发布自己拥有的 EditorGraphSpec v3 Schema,并固定一份 SGCLI `0.3.3` 的 `sgcli.native.v3` 生产快照;SGCLI 同样固定 ASECLI `0.8.1` 快照。双仓兼容检查会比较两组原始 Schema SHA-256,任何漂移都必须先升级生产者再放行。
211
211
 
212
212
  可用 `asecli contract editor-graph --version 3 --spec FILE` 在不连接 Editor 时执行权威 Schema/loader 并返回原文件 SHA。报告分开记录来源快照、发布前复核、生产者 Schema 与真实消费者读取状态。旧 `{graph,report}` wrapper 只允许按已登记形状显式迁移,且内层必须已经是 `sgcli.native.v3`;无版本 discriminator 的旧输入只报告 `legacy_format`,不得伪称已识别版本化 Schema:
213
213
 
@@ -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.0","command":"parse","ok":true,"data":{"node_count":7}}
344
- {"contract_version":1,"cli_version":"0.8.0","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
345
- {"contract_version":1,"cli_version":"0.8.0","command":"version","ok":true,"data":{"version":"0.8.0"}}
343
+ {"contract_version":1,"cli_version":"0.8.1","command":"parse","ok":true,"data":{"node_count":7}}
344
+ {"contract_version":1,"cli_version":"0.8.1","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
345
+ {"contract_version":1,"cli_version":"0.8.1","command":"version","ok":true,"data":{"version":"0.8.1"}}
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.0](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.0)** · [PyPI](https://pypi.org/project/asecli/0.8.0/) · [Agent 操作手册](skills/asecli/SKILL.md) · [SG 转换后整理流程](skills/asecli/references/conversion-workflow.md)
7
+ 当前正式版:**[0.8.1](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.1)** · [PyPI](https://pypi.org/project/asecli/0.8.1/) · [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.0`(双向消费者原生裸 JSON 规格直连与可审计发布证据),可从 [PyPI](https://pypi.org/project/asecli/0.8.0/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.0) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.7.0` 保留为回滚点。
111
+ 当前正式版本为 `0.8.1`(透明 Multiply 的 matcher-v4 语义修复与转换认证加固),可从 [PyPI](https://pypi.org/project/asecli/0.8.1/) 或 [GitHub Release](https://github.com/seeseeczl/ASECLI/releases/tag/v0.8.1) 安装。历史版本见 [GitHub Releases](https://github.com/seeseeczl/ASECLI/releases),`v0.8.0` 保留为回滚点。
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.0-py3-none-any.whl
141
+ uv tool install /path/to/asecli-0.8.1-py3-none-any.whl
142
142
  asecli --help
143
143
  ```
144
144
 
@@ -183,7 +183,7 @@ sgcli sg create Assets/ConvertedNew.shadergraph \
183
183
 
184
184
  `export-sg` 自动从源 Shader 向上识别 Unity/Tuanjie 工程;跨工程资源解析时显式传 `--target-project`。它强制生成 `<stem>.asecli-to-sgcli.spec.json`、`.report.json` 与 `.receipt.json`,同名即失败;receipt 只证明 spec/report 两份文件的提交状态与 SHA,不是中间交接包。命令不调用 SGCLI,也不生成 ASE 自有中间 JSON。未知节点、Master/Pass、外部属性或纹理 Importer 语义时只写规范报告,不写半成品 spec。反方向 `.asecli-to-sgcli.spec.json` 不能交给 ASECLI `create --spec`。
185
185
 
186
- ASECLI 随 wheel 发布自己拥有的 EditorGraphSpec v3 Schema,并固定一份 SGCLI `0.3.0` 的 `sgcli.native.v3` 生产快照;SGCLI 同样固定 ASECLI `0.8.0` 快照。双仓兼容检查会比较两组原始 Schema SHA-256,任何漂移都必须先升级生产者再放行。
186
+ ASECLI 随 wheel 发布自己拥有的 EditorGraphSpec v3 Schema,并固定一份 SGCLI `0.3.3` 的 `sgcli.native.v3` 生产快照;SGCLI 同样固定 ASECLI `0.8.1` 快照。双仓兼容检查会比较两组原始 Schema SHA-256,任何漂移都必须先升级生产者再放行。
187
187
 
188
188
  可用 `asecli contract editor-graph --version 3 --spec FILE` 在不连接 Editor 时执行权威 Schema/loader 并返回原文件 SHA。报告分开记录来源快照、发布前复核、生产者 Schema 与真实消费者读取状态。旧 `{graph,report}` wrapper 只允许按已登记形状显式迁移,且内层必须已经是 `sgcli.native.v3`;无版本 discriminator 的旧输入只报告 `legacy_format`,不得伪称已识别版本化 Schema:
189
189
 
@@ -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.0","command":"parse","ok":true,"data":{"node_count":7}}
320
- {"contract_version":1,"cli_version":"0.8.0","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
321
- {"contract_version":1,"cli_version":"0.8.0","command":"version","ok":true,"data":{"version":"0.8.0"}}
319
+ {"contract_version":1,"cli_version":"0.8.1","command":"parse","ok":true,"data":{"node_count":7}}
320
+ {"contract_version":1,"cli_version":"0.8.1","command":"parse","ok":false,"error":{"code":"NOT_FOUND","message":"..."}}
321
+ {"contract_version":1,"cli_version":"0.8.1","command":"version","ok":true,"data":{"version":"0.8.1"}}
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`。
@@ -43,6 +43,6 @@ evidence: [docs/05-audits/2026-09-01-122217-optimization-tasks.md]
43
43
  | CR-0027 | 将随 wheel 分发的 ASECLI Skill 从 Codex 默认安装扩展为主流 Agent Skills 通用安装 | MOD-CLI MOD-SKILL | 新增 `--agent`、`--scope`、`--project-root`;开放 `.agents/skills` 覆盖 Codex/Cursor/Gemini CLI/GitHub Copilot,Claude Code 使用 `.claude/skills`;无参数与 `--skill-root` 保持兼容;`all` 先检查所有内容冲突 | REG-0058 | 禁用新路由参数即可退回 CR-0015 的 Codex 默认目录;不删除或覆盖已安装 Skill | 定向 46 passed、Skill validator、Project Architect strict 与隔离 wheel 安装通过;BUG-0023 已修复固定片段编译与 SHA 回归;全量 358 passed/3 skipped;发布追溯 REL-0013 |
44
44
  | CR-0028 | 补齐 SG JSON 创建后的 ASE 图整理工作流:端口级复用计划、折叠接口计算基线、刚性计算岛排布与分层验收 | MOD-CORE MOD-LAYOUT MOD-CLI MOD-SKILL | 只读 graph-review;fanout 必须显式选择,默认消费组规则不变;island-columns 默认 1;不重建现有 Shader,不修改 SGCLI,不猜动态节点字段 | REG-0060 REG-0061 | 不用新增参数即保留原布局;回退本任务文件增量,不覆盖并行创建器改动;不发布 | 自动验证中;Local Var 自动 Editor 创建未实现,画布/GUI/渲染验收待具体目标,不以只读计划冒充写入 |
45
45
  | CR-0029 | 强化 CLI JSON 输出契约:原子 ASCII-safe 写出、拒绝非有限数、统一契约/命令/版本字段与错误码注册表 | MOD-CORE MOD-LAYOUT MOD-BRIDGE MOD-CLI MOD-SKILL | 除显式 `--help` 外 stdout 恒为一个严格 JSON;`--version` 改为 JSON;新增顶层字段为加性;损坏几何失败关闭 | REG-0062 | 回滚到 `v0.6.4`;不改 Shader 计算、Editor 交易或用户资产 | 对抗审计 5 项 finding 均关闭;定向 76 passed,全量 388 passed/3 skipped,wheel/ASCII/中文错误路径 smoke 通过;发布见 REL-0015 |
46
- | CR-0030 | 两 CLI 取消外部中间包:ASECLI 直接输出 SGCLI 原生裸 `sgcli.native.v3`,并以方向化 spec/report 文件名与 SGCLI 的 EditorGraphSpec 输出对称 | MOD-CLI MOD-BRIDGE MOD-SG-EXPORT MOD-CONTRACT MOD-SKILL | 新增 `export-sg`、`contract`;`create --spec` 拒绝 wrapper/report/反方向后缀;旧历史 JSON 不进入正式流程;候选 wheel Schema/篡改负例、发布 receipt 与跨仓 required workflow 失败关闭;不调用或依赖 SGCLI 运行时 | REG-0063 | 禁用新入口或回滚 wheel;不得恢复消费者隐式解包/正式 translator,不批量改名历史文件 | `v0.8.0` 发布候选:第三轮补齐只读 Deploy Key、精确 SGCLI commit、build-once wheel、WaveNoise 语义 fixture、receipt 交叉绑定、fixture-set 完整性、Draft 数值等价与 clean provenance;全量 `428 passed, 3 skipped`(Python 3.10 同结果)。远端发布与本机安装执行中;最终 wheel Editor/画布/渲染仍开放 |
46
+ | CR-0030 | 两 CLI 取消外部中间包:ASECLI 直接输出 SGCLI 原生裸 `sgcli.native.v3`,并以方向化 spec/report 文件名与 SGCLI 的 EditorGraphSpec 输出对称 | MOD-CLI MOD-BRIDGE MOD-SG-EXPORT MOD-CONTRACT MOD-SKILL | 新增 `export-sg`、`contract`;`create --spec` 拒绝 wrapper/report/反方向后缀;旧历史 JSON 不进入正式流程;候选 wheel Schema/篡改负例、发布 receipt 与跨仓 required workflow 失败关闭;不调用或依赖 SGCLI 运行时 | REG-0063 | 禁用新入口或回滚 wheel;不得恢复消费者隐式解包/正式 translator,不批量改名历史文件 | `v0.8.0@58ccf10` 已发布:publish run 34592028675、GitHub/PyPI 同源资产、本机 CLI/Skill 和 WaveNoise 双向原文件 SHA 全部复核通过;全量 `428 passed, 3 skipped`(Python 3.10 同结果)。最终 wheel Editor/画布/渲染仍开放,不声明视觉等价 |
47
47
 
48
48
  授权来源:用户于 2026-09-01 明确要求“执行优化任务,全部完成”,并于同日批准吸收 ASE Editor API 方案后要求“执行”;随后明确要求先推送并执行对抗性审计优化计划。2026-09-02 用户要求推送并进一步明确“以正式的 CLI 形式发布”,据此创建私有正式 GitHub Release `v0.1.0`;同日进一步明确说明条样式一致性必须成为 CLI 的规范能力,据此登记 CR-0011/BUG-0015,并明确所有通过 CLI 构建的 ASE 文件都必须使用中文属性显示名、自动变量名与默认值 Tooltip 和中文使用说明,据此登记 CR-0012。同日用户要求执行 `2026-09-02-211218` 优化计划,据此登记 CR-0013;随后书面授权打 tag 并发布 `v0.2.0`。2026-09-03 用户要求执行最新优化任务书并把剩余门槛全部完成,据此登记 CR-0014 并授权完成必要提交、直接推送与远程 CI 验证;随后明确要求“推送并发布”,据此授权创建私有正式版本 `v0.3.0`。2026-09-04 用户要求将 ASE 精排与 Inspector 编排规范作为可安装 Skill 并授权发布 `v0.3.1`。2026-09-05 用户要求执行 MZGUI 对接与效率优化,随后分别授权私有版本 `v0.4.0`、`v0.4.1` 和审计补丁 `v0.4.2`。2026-09-06 用户明确 Tooltip 必填、HelpBox 可选、条件置灰并授权 `v0.5.0`;随后要求脑图式 DAG 精排并授权 `v0.6.0`;又将 Local Var 门槛改为两个及以上不同消费组,据此登记 CR-0020;最后以多组人工示范明确递归局部鱼骨、Comment 保留和显式 WireNode 路由并要求执行,据此登记 CR-0021。2026-09-07 用户明确授权提交、推送、私有 Release `v0.6.1` 和本机安装。同日进一步明确要求开源该项目并提供同事安装命令,据此登记 CR-0022:采用 MIT 许可证并将 GitHub 仓库转为 public。随后要求做成最终、最常见的安装形态,据此登记 CR-0023:发布 PyPI 项目 `asecli`,用户命令为 `uv tool install asecli`;仍未授权 CLI Hub。2026-09-08 用户进一步明确要求推送、发布新版本并安装本机,据此授权 `v0.6.2` 的 main 推送、tag、GitHub Release、PyPI Trusted Publishing 与本机正式产物安装;CLI Hub 仍不在授权范围。首次 PyPI OIDC 因没有对应账号和 Trusted Publisher 失败后,用户说明没有 PyPI 账号并明确要求继续 GitHub Release 发布,据此以 CR-0025 将 PyPI 延后并完成 GitHub-only 交付。随后用户取得 PyPI 账号并明确要求继续配置,据此登记 CR-0026,启用精确匹配的 Pending Publisher 并通过 run 34192454152 attempt 3 完成首次 PyPI 发布。同日用户要求将现有 Skill 改为主流 Agent 通用,据此登记 CR-0027;本次不包含推送、tag 或发布。2026-09-10 用户要求对 JSON 输出能力执行对抗审计与优化,随后明确要求推送、发布并安装本机,据此登记 CR-0029 并授权 `v0.7.0` GitHub Release、PyPI Trusted Publishing 与本机 CLI/Skill 安装;CLI Hub 仍不在范围。
@@ -95,4 +95,5 @@
95
95
  | 2026-09-10 | v0.7.0 双渠道发布与本机安装完成 | tag `v0.7.0` 解引用为 `029fea4`;run 34475832000 的双 Python verify、package 与 PyPI OIDC publish 全成功;GitHub Release 为 Latest,五项资产与 PyPI wheel/sdist 摘要一致;本机 CLI、官方索引 uvx 及三处全局 Skill 均通过,旧 Skill 已完整备份 | REL-0015 CR-0029 TASK-0064 REG-0062 |
96
96
  | 2026-09-11 | 双向裸规格直连第二轮对抗整改 | CR-0030 / ADR-0022 / TASK-0065 / REG-0063:完成 AA-OPT-201~207,补齐 Draft parity、候选 wheel 契约可信根、篡改负例、required workflow、语义矩阵、离线 artifact、发布 receipt 与迁移 provenance。全量 `419 passed, 3 skipped`;最终 wheel `2e38ef11...` 可复现,真实双向 WaveNoise producer/consumer SHA 一致。用户级 ASECLI Skill 因不同内容按规则拒绝覆盖;最终 wheel Editor/画布/render 仍开放。未提交、未推送、未发布。 | CR-0030 ADR-0022 TASK-0065 REG-0063 AA-OPT-201 AA-OPT-202 AA-OPT-203 AA-OPT-204 AA-OPT-205 AA-OPT-206 AA-OPT-207 |
97
97
  | 2026-09-11 | 双向裸规格直连第三轮对抗整改 | 执行 AA-OPT-301~308:public PR 不读取 private SGCLI;可信链要求只读 token 和精确 40 位 SHA;package wheel 原样交给 cross-CLI/publish;WaveNoise 双向语义 fixture、receipt 绑定、fixture-set 完整性、Draft 数值等价与 clean provenance 通过。全量及 Python 3.10 均 `428 passed, 3 skipped`;本地 clean release-mode 双 wheel matrix/verifier 通过。AA-OPT-309 的远端 ruleset/required checks、最终 wheel Editor/画布/render 仍开放。未提交、未推送、未发布。 | CR-0030 TASK-0065 REG-0063 AA-OPT-301 AA-OPT-302 AA-OPT-303 AA-OPT-304 AA-OPT-305 AA-OPT-306 AA-OPT-307 AA-OPT-308 AA-OPT-309 |
98
- | 2026-09-11 | v0.8.0 发布候选 | 用户授权两个 CLI 分别推送、发布并更新本机;ASECLI 以 pre-1.0 次版本发布双向裸 JSON 与第三轮门禁,保留 v0.7.0 回滚点。私有 SGCLI 跨仓认证使用专用只读 Deploy Key;本地双 Python、可复现构建和 clean release-mode 双 wheel matrix/verifier 已通过。远端 publish/OIDC、双渠道回读与本机安装继续执行;最终 Editor/画布/render 保持开放披露。 | REL-0016 CR-0030 TASK-0065 REG-0063 |
98
+ | 2026-09-11 | v0.8.0 双渠道发布与本机安装完成 | tag `v0.8.0` 解引用到 `58ccf10`;[publish run 34592028675](https://github.com/seeseeczl/ASECLI/actions/runs/34592028675) 的双 Python、package、public contract、Deploy Key cross-CLI 和 PyPI OIDC 全绿。GitHub Release 五项资产与 PyPI wheel/sdist 摘要一致且未 yanked;本机 CLI 为 0.8.0,`.agents`/`.claude` Skill 与 wheel 一致,旧三处 Skill 已备份。WaveNoise 双向生产/消费 SHA 一致;最终 wheel 的 Editor/画布/render 保持未验证披露。 | REL-0016 CR-0030 TASK-0065 REG-0063 |
99
+ | 2026-09-14 | v0.8.1 发布候选 | matcher-v4 识别并折叠原生 `Lerp(white,color,alpha)`,保留 Wave Noise 不可表示 Alpha Blend 的失败关闭;SGCLI 普通 authoring 与转换认证分离。发布前全量 `461 passed, 3 skipped`,CI governance、供应链与 diff 通过;远端 publish/PyPI、正式资产回读和本机安装待执行,Editor/画布/render 保持开放披露。 | REL-0017 CR-0030 TASK-0066 REG-0063 |
@@ -62,4 +62,4 @@ CR-0027,变更,已发布,ADR-0021,MOD-CLI MOD-SKILL,TASK-0059 TASK-0060,REG-0058
62
62
  BUG-0023,缺陷,已修复,ADR-0020,MOD-BRIDGE,TASK-0060,REG-0059,2026-09-08 两类真实 v2 CLI create/manifest/validate/recompile 通过;358 passed/3 skipped,REL-0013,long,方法体兼容 KeyValuePair;SphereMask Radius 重载仍为已知限制
63
63
  CR-0028,变更,已发布(视觉与自动LocalVar仍待验),ADR-0021,MOD-CORE MOD-LAYOUT MOD-CLI MOD-SKILL,TASK-0061 TASK-0062 TASK-0063,REG-0060 REG-0061,端口级基线与复用计划及岛内刚性多列布局;未自动创建LocalVar;未做实机验收,REL-0014,long,保留并行创建器改动;不修改SGCLI
64
64
  CR-0029,变更,已发布,ADR-0001 ADR-0009,MOD-CORE MOD-LAYOUT MOD-BRIDGE MOD-CLI MOD-SKILL,TASK-0064,REG-0062,2026-09-10 v0.7.0@029fea4;run 34475832000 全成功;GitHub/PyPI 摘要一致;本机 CLI/Skill 安装幂等,REL-0015,long,严格原子 JSON;非有限数拒绝;version JSON;22 错误码;17 子命令动态覆盖
65
- CR-0030,变更,发布候选(视觉门禁开放),ADR-0022,MOD-CLI MOD-BRIDGE MOD-SG-EXPORT MOD-CONTRACT MOD-SKILL,TASK-0065,REG-0063,2026-09-11 v0.8.0 候选 428 passed/3 skipped;Python 3.10 同结果;clean release-mode 双 wheel WaveNoise/receipt/Schema/artifact verifier 通过,REL-0016,long,消费者原生裸规格与方向命名;远端发布与本机安装执行中
65
+ CR-0030,变更,已发布(视觉门禁开放),ADR-0022,MOD-CLI MOD-BRIDGE MOD-SG-EXPORT MOD-CONTRACT MOD-SKILL,TASK-0065,REG-0063,2026-09-11 v0.8.0 publish run 34592028675 全绿;GitHub/PyPI 资产、本机 CLI/Skill 和 WaveNoise 双向原文件 SHA 通过,REL-0016,long,消费者原生裸规格与方向命名;最终 wheel Editor/画布/渲染仍开放
@@ -105,7 +105,8 @@ kickoff_completion: complete
105
105
  | TASK-0062 | CR-0028 | 独立计算岛刚性多列摆放 | TASK-0061;已有鱼骨与真实几何契约 | island_layout.py/meticulous_layout.py 与测试 | 拆改岛内连接、自动 WireNode 路由、虚构几何 | long/2026-09-08 | REG-0061;相对位置、依赖与二次幂等 | 几何缺失/布局硬失败时零写入 | 定向 94 passed;自动环节已验证;实机待验 | 自动验证完成,实机待验 |
106
106
  | TASK-0063 | CR-0028 | 将转换后交付流程接入 CLI 与 Skill | TASK-0061 TASK-0062;用户完整过程 | graph_review_command.py/main.py/layout_command.py;Skill/README/治理 | 发布、覆盖用户 Skill、改并行创建器、生产 Shader | long/2026-09-08 | REG-0060 REG-0061;CLI JSON/只读/失败码、Skill validator | 计划冒充创建或结构冒充视觉时停止 | 定向 94 passed;自动环节已验证;未接入自动 Local Var Editor 创建 | 自动验证完成,实机待验 |
107
107
  | TASK-0064 | CR-0029 | 关闭 JSON 输出对抗审计并发布安装 `v0.7.0` | AA-OPT-001~005;用户推送、发布、安装授权 | contract/main;Commentary/layout/Editor 几何;README/Skill;版本与 REL-0015 | CLI Hub;修改用户 Shader;覆盖未知 Skill;绕过发布门禁 | long/2026-09-10 | REG-0062;双 Python、完整治理、可复现构建、远程 publish/OIDC、双渠道摘要、本机 CLI/Skill | CI、OIDC、摘要、安装或 Skill 冲突任一失败即停止,保留 `v0.6.4` | v0.7.0@029fea4;run 34475832000 全成功;GitHub/PyPI 摘要一致;本机 CLI/Skill 安装与幂等通过 | 已发布 |
108
- | TASK-0065 | CR-0030 ADR-0022 AA-OPT-201~207 AA-OPT-301~309 REL-0016 | 实现 ASECLI→native v3 与 SGCLI→EditorGraphSpec v3 的双向裸规格原文件直连并发布 0.8.0 | 用户确认的合并优化计划、三轮对抗审计与推送发布安装授权 | `export_sg.py`、`sg_export/**`、`contracts/**`、`schema_validation.py`、`conversion_output.py`、CLI/create、README/Skill、测试、治理和 cross-CLI workflows | 调用 SGCLI;恢复 source-native/wrapper 主链;修改生产 Shader;覆盖用户 dirty Skill;绕过失败门禁 | long/2026-09-11 | REG-0063;Draft parity;候选 wheel Schema/篡改负例;固定 WaveNoise 语义;artifact 完整性与 clean provenance;双 Python全量;远端 publish/OIDC;双渠道回读;本机 CLI/Skill;Editor/画布/渲染分层 | 未知 ASE/Target/资源/端口、部分 spec、同名覆盖、Schema 漂移、脏候选、远端门禁或资产冲突即停止 | 0.8.0 候选全量 428 passed/3 skipped;0.3.0/0.8.0 wheel 可复现且本地 clean matrix/verifier 通过;只读 Deploy Key 已配置;远端发布与安装执行中;最终 Editor/画布/渲染开放披露 | 发布候选(REL-0016) |
108
+ | TASK-0065 | CR-0030 ADR-0022 AA-OPT-201~207 AA-OPT-301~309 REL-0016 | 实现 ASECLI→native v3 与 SGCLI→EditorGraphSpec v3 的双向裸规格原文件直连并发布 0.8.0 | 用户确认的合并优化计划、三轮对抗审计与推送发布安装授权 | `export_sg.py`、`sg_export/**`、`contracts/**`、`schema_validation.py`、`conversion_output.py`、CLI/create、README/Skill、测试、治理和 cross-CLI workflows | 调用 SGCLI;恢复 source-native/wrapper 主链;修改生产 Shader;覆盖用户 dirty Skill;绕过失败门禁 | long/2026-09-11 | REG-0063;Draft parity;候选 wheel Schema/篡改负例;固定 WaveNoise 语义;artifact 完整性与 clean provenance;双 Python全量;远端 publish/OIDC;双渠道回读;本机 CLI/Skill;Editor/画布/渲染分层 | 未知 ASE/Target/资源/端口、部分 spec、同名覆盖、Schema 漂移、脏候选、远端门禁或资产冲突即停止 | `v0.8.0@58ccf10` 已发布;run 34592028675、GitHub/PyPI 资产、本机 CLI/Skill 和 WaveNoise 双向原文件 SHA 全部复核通过;最终 Editor/画布/渲染开放披露 | 已发布(REL-0016,视觉开放) |
109
+ | TASK-0066 | CR-0030 ADR-0022 AA-OPT-201 AA-OPT-202 REL-0017 | 发布并安装 matcher-v4 与 ASE→SG 转换认证补丁 v0.8.1 | 用户要求两个 CLI 分别推送、发布并安装本机 | `sg_export/**`、report、版本、README、回归与发布证据 | 修改生产 Shader;绕过不可表示 Alpha Blend;恢复 wrapper;覆盖未知 Skill | long/2026-09-14 | REG-0063;两仓全量、治理、供应链、Tag publish、GitHub/PyPI 摘要、本机 CLI/Skill;Editor/视觉分层 | 任一自动/远端/摘要/安装门禁失败即停止并保留 v0.8.0 | 本地候选 `461 passed, 3 skipped`,远端与安装待执行 | verified |
109
110
 
110
111
  ## 验证、风险与回滚
111
112
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asecli"
3
- version = "0.8.0"
3
+ version = "0.8.1"
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"
@@ -21,7 +21,7 @@ if ($env:ASECLI_REF) {
21
21
  $tag = (Invoke-RestMethod "https://api.github.com/repos/$repo/releases/latest").tag_name
22
22
  }
23
23
  if (-not $tag) {
24
- $tag = "v0.8.0"
24
+ $tag = "v0.8.1"
25
25
  }
26
26
 
27
27
  $version = $tag.TrimStart("v")
@@ -32,7 +32,7 @@ else
32
32
  TAG=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)
33
33
  fi
34
34
  if [ -z "$TAG" ]; then
35
- TAG="v0.8.0"
35
+ TAG="v0.8.1"
36
36
  fi
37
37
  VERSION=${TAG#v}
38
38
  WHEEL="https://github.com/${REPO}/releases/download/${TAG}/asecli-${VERSION}-py3-none-any.whl"
@@ -1,3 +1,3 @@
1
1
  """AseCLI — agent-native toolkit for Amplify Shader Editor assets."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.8.1"
@@ -0,0 +1,113 @@
1
+ """Recognize the certified single-assignment AlphaModulate HLSL subset."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def inputs(function: dict | None) -> tuple[str, str] | None:
9
+ if not function or function.get("source") != "String":
10
+ return None
11
+ ports = function.get("ports", [])
12
+ outputs = [port for port in ports if port.get("direction") == "Output"]
13
+ if len(outputs) != 1 or outputs[0].get("type") != "Vector3":
14
+ return None
15
+ output_name = outputs[0].get("name")
16
+ if not isinstance(output_name, str) or not re.fullmatch(r"[A-Za-z_]\w*", output_name):
17
+ return None
18
+ body = _strip_comments(function.get("body", ""))
19
+ if body is None:
20
+ return None
21
+ match = re.fullmatch(
22
+ rf"\s*{re.escape(output_name)}\s*=\s*lerp\s*\((.*)\)\s*;\s*",
23
+ body,
24
+ flags=re.DOTALL,
25
+ )
26
+ if not match:
27
+ return None
28
+ args = _split_arguments(match.group(1))
29
+ if len(args) != 3 or not _is_white(args[0]):
30
+ return None
31
+ color_name, alpha_name = args[1].strip(), args[2].strip()
32
+ if not re.fullmatch(r"[A-Za-z_]\w*", color_name):
33
+ return None
34
+ if not re.fullmatch(r"[A-Za-z_]\w*", alpha_name):
35
+ return None
36
+ input_types = {
37
+ port.get("name"): port.get("type")
38
+ for port in ports if port.get("direction") == "Input"
39
+ }
40
+ if input_types.get(color_name) != "Vector3" or input_types.get(alpha_name) != "Vector1":
41
+ return None
42
+ return color_name, alpha_name
43
+
44
+
45
+ def _split_arguments(value: str) -> list[str]:
46
+ result, start, depth = [], 0, 0
47
+ for index, char in enumerate(value):
48
+ if char == "(":
49
+ depth += 1
50
+ elif char == ")":
51
+ depth -= 1
52
+ if depth < 0:
53
+ return []
54
+ elif char == "," and depth == 0:
55
+ result.append(value[start:index])
56
+ start = index + 1
57
+ if depth != 0:
58
+ return []
59
+ result.append(value[start:])
60
+ return result
61
+
62
+
63
+ def _is_white(value: str) -> bool:
64
+ compact = re.sub(r"\s+", "", value)
65
+ constructor = re.fullmatch(r"(?:float|half)3\((.*)\)", compact, flags=re.IGNORECASE)
66
+ if constructor:
67
+ parts = _split_arguments(constructor.group(1))
68
+ return len(parts) == 3 and all(_is_one(part) for part in parts)
69
+ scalar = re.fullmatch(r"\(?([^()]+)\)?\.xxx", compact, flags=re.IGNORECASE)
70
+ return bool(scalar and _is_one(scalar.group(1)))
71
+
72
+
73
+ def _is_one(value: str) -> bool:
74
+ try:
75
+ return float(value.rstrip("fF")) == 1.0
76
+ except ValueError:
77
+ return False
78
+
79
+
80
+ def _strip_comments(value: str) -> str | None:
81
+ result, index, state = [], 0, "code"
82
+ while index < len(value):
83
+ char = value[index]
84
+ following = value[index + 1] if index + 1 < len(value) else ""
85
+ if state == "code":
86
+ if char == '"':
87
+ state = "string"
88
+ result.append(char)
89
+ elif char == "/" and following in {"/", "*"}:
90
+ result.extend(" ")
91
+ index += 1
92
+ state = "line_comment" if following == "/" else "block_comment"
93
+ else:
94
+ result.append(char)
95
+ elif state == "string":
96
+ result.append(char)
97
+ if char == "\\" and following:
98
+ result.append(following)
99
+ index += 1
100
+ elif char == '"':
101
+ state = "code"
102
+ elif state == "line_comment":
103
+ result.append(char if char in "\r\n" else " ")
104
+ if char in "\r\n":
105
+ state = "code"
106
+ elif char == "*" and following == "/":
107
+ result.extend(" ")
108
+ index += 1
109
+ state = "code"
110
+ else:
111
+ result.append(char if char in "\r\n" else " ")
112
+ index += 1
113
+ return None if state in {"string", "block_comment"} else "".join(result)
@@ -7,6 +7,8 @@ import json
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
+ from .report_semantics import check_evidence
11
+
10
12
 
11
13
  CHECK_RULES = {
12
14
  "properties": "SEM-PROP-001",
@@ -74,7 +76,7 @@ def build_public_report(
74
76
  name: {
75
77
  "rule": rule,
76
78
  "status": check_status[name],
77
- "evidence": _check_evidence(name, check_status[name], internal, failure),
79
+ "evidence": check_evidence(name, check_status[name], internal, failure),
78
80
  }
79
81
  for name, rule in CHECK_RULES.items()
80
82
  }
@@ -188,6 +190,9 @@ def _alpha_equation_verified(internal: dict[str, Any] | None) -> bool:
188
190
  target = internal.get("target_mapping", {})
189
191
  if target.get("surface") == "Opaque":
190
192
  return True
193
+ semantics = internal.get("surface_semantics", {})
194
+ if semantics.get("alpha_blend_equivalent") is not True or semantics.get("unresolved"):
195
+ return False
191
196
  return any(
192
197
  isinstance(item, dict)
193
198
  and item.get("block") == "SurfaceDescription.Alpha"
@@ -199,31 +204,16 @@ def _blend_equation_verified(internal: dict[str, Any] | None) -> bool:
199
204
  if not isinstance(internal, dict):
200
205
  return False
201
206
  target = internal.get("target_mapping", {})
207
+ semantics = internal.get("surface_semantics", {})
208
+ if target.get("surface") == "Transparent" and (
209
+ semantics.get("rgb_blend_equivalent") is not True
210
+ or semantics.get("alpha_blend_equivalent") is not True
211
+ ):
212
+ return False
213
+ if semantics.get("unresolved"):
214
+ return False
215
+ if semantics.get("target_pipeline", {}).get("implicit_alpha_modulate"):
216
+ source_explicit = semantics.get("source", {}).get("explicit_alpha_modulate", False)
217
+ if source_explicit and not semantics.get("rewrites"):
218
+ return False
202
219
  return target.get("blend") in {"Alpha", "Premultiply", "Additive", "Multiply"}
203
-
204
-
205
- def _check_evidence(
206
- name: str,
207
- state: str,
208
- internal: dict[str, Any] | None,
209
- failure: str | None,
210
- ) -> str:
211
- if state == "verified":
212
- return {
213
- "properties": "ASE properties reconciled with ShaderLab declarations",
214
- "resources": "all texture dependencies are null or have matching importer evidence",
215
- "graph_algorithm": "certified ASE nodes, defaults and wires were deterministically mapped",
216
- "target": "certified URP Unlit Master settings were mapped",
217
- "passes": "the certified ASE URP Unlit pass set was recognized",
218
- "alpha_equation": "opaque alpha behavior or the explicit Alpha block binding was preserved",
219
- "blend_equation": "the certified ASE Master blend mode was mapped to the SG target",
220
- }[name]
221
- if failure:
222
- return f"conversion stopped before this invariant was proved: {failure}"
223
- diagnostics = internal.get("diagnostics", []) if isinstance(internal, dict) else []
224
- if diagnostics:
225
- first = diagnostics[0]
226
- return f"{first.get('code', 'UNPROVEN')}: {first.get('reason', 'not proven')}"
227
- if name in {"alpha_equation", "blend_equation"}:
228
- return "the source target equation could not be proved from the certified Master mapping"
229
- return "not proven by the producer-only export"
@@ -14,6 +14,7 @@ from .model import (
14
14
  from .nodes import convert_node
15
15
  from .sources import compiled_properties, load_asset_map
16
16
  from .evidence import inspector_degradations, record_port, validate_template_passes
17
+ from .surface_semantics import normalize_surface_outputs
17
18
  from .reconciliation import (
18
19
  groups, reconcile_shaderlab_properties, unique_properties, verify_texture_dependencies,
19
20
  )
@@ -123,6 +124,7 @@ def build_candidate(
123
124
  for node in nodes:
124
125
  for target_id in node.source_ids:
125
126
  edges.append(SemanticEdge(node.target_id, 0, target_id, -1))
127
+ nodes, edges = normalize_surface_outputs(nodes, edges, target_spec, mappings, report, text)
126
128
  groups_spec = groups(ase, mappings, diagnostics)
127
129
  report["output_bindings"] = [
128
130
  {"block": edge.target_id, "port": edge.target_port}
@@ -243,8 +245,6 @@ def _template_option(fields, label, choices, diagnostics, node_id):
243
245
 
244
246
 
245
247
  def _template_bool(fields, label, diagnostics, node_id):
246
- value = _template_option(fields, label, (False, True), diagnostics, node_id)
247
- return value
248
-
248
+ return _template_option(fields, label, (False, True), diagnostics, node_id)
249
249
  def _graph_precision(master):
250
250
  return "Half" if master and len(master.raw_fields) > 4 and master.raw_fields[4] == "Half" else "Single"
@@ -12,7 +12,7 @@ from typing import Any
12
12
  from .. import __version__
13
13
 
14
14
  REPORT_SCHEMA = "sgcli.shader-conversion-report.v1"
15
- MAPPING_VERSION = 3
15
+ MAPPING_VERSION = 4
16
16
  SUPPORTED_ASE_VERSIONS = frozenset({"19109", "19602"})
17
17
  MASTER_TYPE = "AmplifyShaderEditor.TemplateMultiPassMasterNode"
18
18
  WIRE_TYPE = "AmplifyShaderEditor.WireNode"
@@ -0,0 +1,200 @@
1
+ """ShaderLab and standard URP Shader Graph blend-state semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ _BLEND_FACTORS = {
9
+ value.lower(): value
10
+ for value in (
11
+ "Zero", "One", "SrcColor", "OneMinusSrcColor", "SrcAlpha",
12
+ "OneMinusSrcAlpha", "DstColor", "OneMinusDstColor", "DstAlpha",
13
+ "OneMinusDstAlpha", "SrcAlphaSaturate",
14
+ )
15
+ }
16
+
17
+
18
+ def record_pass_blend(semantics: dict, target: dict, source_text: str | None) -> None:
19
+ evidence = forward_blend_evidence(source_text or "")
20
+ source = evidence.get("blend")
21
+ target_blend = target_blend_factors(target)
22
+ semantics["source_pass_blend"] = source
23
+ semantics["target_pass_blend"] = target_blend
24
+ semantics["pass_blend_parser"] = {
25
+ key: value for key, value in evidence.items() if key != "blend"
26
+ }
27
+ if source is not None and target_blend is not None:
28
+ semantics["rgb_blend_equivalent"] = source["rgb"] == target_blend["rgb"]
29
+ semantics["alpha_blend_equivalent"] = source["alpha"] == target_blend["alpha"]
30
+
31
+
32
+ def forward_blend(source_text: str) -> dict | None:
33
+ """Return the unique Forward pass blend factors, or None when unproved."""
34
+ return forward_blend_evidence(source_text).get("blend")
35
+
36
+
37
+ def forward_blend_evidence(source_text: str) -> dict:
38
+ """Parse a unique Forward Pass without trusting comments or unrelated text."""
39
+ masked, error = _mask_comments(source_text)
40
+ if error:
41
+ return _evidence("malformed", 0, error)
42
+ bodies, error = _pass_bodies(masked)
43
+ if error:
44
+ return _evidence("malformed", 0, error)
45
+ search_bodies = bodies or [masked]
46
+ forward = [body for body in search_bodies if _is_forward(body)]
47
+ if not forward:
48
+ return _evidence("missing", 0, "no Forward pass was found")
49
+ if len(forward) != 1:
50
+ return _evidence(
51
+ "ambiguous", len(forward), f"expected one Forward pass, found {len(forward)}"
52
+ )
53
+ blend, status, reason, directive_count = _blend_in_pass(forward[0])
54
+ result = _evidence(status, 1, reason)
55
+ result["directive_count"] = directive_count
56
+ if blend is not None:
57
+ result["blend"] = blend
58
+ return result
59
+
60
+
61
+ def target_blend_factors(target: dict) -> dict | None:
62
+ if target.get("surface") != "Transparent":
63
+ return {"rgb": ["One", "Zero"], "alpha": ["One", "Zero"]}
64
+ return {
65
+ "Alpha": {"rgb": ["SrcAlpha", "OneMinusSrcAlpha"], "alpha": ["One", "OneMinusSrcAlpha"]},
66
+ "Premultiply": {"rgb": ["One", "OneMinusSrcAlpha"], "alpha": ["One", "OneMinusSrcAlpha"]},
67
+ "Additive": {"rgb": ["SrcAlpha", "One"], "alpha": ["One", "One"]},
68
+ "Multiply": {"rgb": ["DstColor", "Zero"], "alpha": ["Zero", "One"]},
69
+ }.get(target.get("blend"))
70
+
71
+
72
+ def _evidence(status: str, count: int, reason: str) -> dict:
73
+ return {
74
+ "status": status,
75
+ "candidate_count": count,
76
+ "reason": reason,
77
+ "parser_version": 2,
78
+ }
79
+
80
+
81
+ def _mask_comments(source: str) -> tuple[str, str | None]:
82
+ """Replace comments with spaces while preserving offsets, strings and newlines."""
83
+ chars = list(source)
84
+ index, state = 0, "code"
85
+ while index < len(source):
86
+ char = source[index]
87
+ following = source[index + 1] if index + 1 < len(source) else ""
88
+ if state == "code":
89
+ if char == '"':
90
+ state = "string"
91
+ elif char == "/" and following == "/":
92
+ chars[index] = chars[index + 1] = " "
93
+ index += 1
94
+ state = "line_comment"
95
+ elif char == "/" and following == "*":
96
+ chars[index] = chars[index + 1] = " "
97
+ index += 1
98
+ state = "block_comment"
99
+ elif state == "string":
100
+ if char == "\\" and following:
101
+ index += 1
102
+ elif char == '"':
103
+ state = "code"
104
+ elif state == "line_comment":
105
+ if char in "\r\n":
106
+ state = "code"
107
+ else:
108
+ chars[index] = " "
109
+ else:
110
+ if char == "*" and following == "/":
111
+ chars[index] = chars[index + 1] = " "
112
+ index += 1
113
+ state = "code"
114
+ elif char not in "\r\n":
115
+ chars[index] = " "
116
+ index += 1
117
+ if state == "block_comment":
118
+ return "".join(chars), "unterminated block comment"
119
+ if state == "string":
120
+ return "".join(chars), "unterminated string literal"
121
+ return "".join(chars), None
122
+
123
+
124
+ def _mask_strings(source: str) -> str:
125
+ chars = list(source)
126
+ index, quoted = 0, False
127
+ while index < len(source):
128
+ char = source[index]
129
+ if not quoted and char == '"':
130
+ chars[index] = " "
131
+ quoted = True
132
+ elif quoted:
133
+ if char == "\\" and index + 1 < len(source):
134
+ chars[index] = chars[index + 1] = " "
135
+ index += 1
136
+ else:
137
+ if char == '"':
138
+ quoted = False
139
+ if char not in "\r\n":
140
+ chars[index] = " "
141
+ index += 1
142
+ return "".join(chars)
143
+
144
+
145
+ def _pass_bodies(source: str) -> tuple[list[str], str | None]:
146
+ structural = _mask_strings(source)
147
+ result, covered_until = [], -1
148
+ for match in re.finditer(r"\bPass\s*\{", structural, flags=re.IGNORECASE):
149
+ if match.start() < covered_until:
150
+ continue
151
+ opening = structural.find("{", match.start(), match.end())
152
+ depth = 0
153
+ for index in range(opening, len(structural)):
154
+ if structural[index] == "{":
155
+ depth += 1
156
+ elif structural[index] == "}":
157
+ depth -= 1
158
+ if depth == 0:
159
+ result.append(source[opening + 1:index])
160
+ covered_until = index + 1
161
+ break
162
+ else:
163
+ return [], "unterminated Pass block"
164
+ return result, None
165
+
166
+
167
+ def _is_forward(body: str) -> bool:
168
+ header = re.split(r"\b(?:HLSLPROGRAM|CGPROGRAM)\b", body, maxsplit=1, flags=re.IGNORECASE)[0]
169
+ return bool(re.search(r'\bName\s+"Forward"(?!\w)', header, flags=re.IGNORECASE))
170
+
171
+
172
+ def _blend_in_pass(body: str) -> tuple[dict | None, str, str, int]:
173
+ header = re.split(r"\b(?:HLSLPROGRAM|CGPROGRAM)\b", body, maxsplit=1, flags=re.IGNORECASE)[0]
174
+ directives = re.findall(r"(?im)^\s*Blend\s+(.+?)\s*$", header)
175
+ if len(directives) > 1:
176
+ return None, "ambiguous", f"expected at most one Blend directive, found {len(directives)}", len(directives)
177
+ if not directives:
178
+ if re.search(r"\bBlend\b", header, flags=re.IGNORECASE):
179
+ return None, "unsupported", "Blend directive is not on a uniquely parseable line", 1
180
+ return _off_blend(), "parsed", "Forward pass uses implicit Blend Off", 0
181
+ value = directives[0].strip()
182
+ if value.lower() == "off":
183
+ return _off_blend(), "parsed", "Forward pass explicitly uses Blend Off", 1
184
+ channels = [part.strip().split() for part in value.split(",")]
185
+ if len(channels) not in (1, 2) or any(len(part) != 2 for part in channels):
186
+ return None, "unsupported", f"unsupported Blend directive: {value}", 1
187
+ normalized = []
188
+ for source, destination in channels:
189
+ pair = (_BLEND_FACTORS.get(source.lower()), _BLEND_FACTORS.get(destination.lower()))
190
+ if None in pair:
191
+ return None, "unsupported", f"unsupported Blend factors: {value}", 1
192
+ normalized.append(list(pair))
193
+ return {
194
+ "rgb": normalized[0],
195
+ "alpha": normalized[1] if len(normalized) == 2 else list(normalized[0]),
196
+ }, "parsed", "unique Forward pass Blend parsed", 1
197
+
198
+
199
+ def _off_blend() -> dict:
200
+ return {"rgb": ["One", "Zero"], "alpha": ["One", "Zero"]}