claudecode-omc 5.6.5 β†’ 5.6.7

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 (434) hide show
  1. package/.local/commands/code-review.md +416 -0
  2. package/.local/commands/pr.md +250 -0
  3. package/.local/commands/prp-pr.md +244 -0
  4. package/.local/guidelines/CLAUDE.md +47 -0
  5. package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  10. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  11. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  12. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  13. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  40. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  41. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  42. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  43. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  44. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  45. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  46. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  47. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  48. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  49. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  50. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  51. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  52. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  53. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  54. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  55. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  56. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  57. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  58. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  59. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  60. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  61. package/bundled/manifest.json +4 -4
  62. package/bundled/upstream/anthropic-skills/skills/algorithmic-art/LICENSE.txt +1 -1
  63. package/bundled/upstream/anthropic-skills/skills/brand-guidelines/LICENSE.txt +1 -1
  64. package/bundled/upstream/anthropic-skills/skills/canvas-design/LICENSE.txt +1 -1
  65. package/bundled/upstream/anthropic-skills/skills/claude-api/LICENSE.txt +1 -1
  66. package/bundled/upstream/anthropic-skills/skills/claude-api/SKILL.md +120 -58
  67. package/bundled/upstream/anthropic-skills/skills/claude-api/curl/examples.md +9 -9
  68. package/bundled/upstream/anthropic-skills/skills/claude-api/curl/managed-agents.md +336 -0
  69. package/bundled/upstream/anthropic-skills/skills/claude-api/go/managed-agents/README.md +561 -0
  70. package/bundled/upstream/anthropic-skills/skills/claude-api/java/claude-api.md +2 -2
  71. package/bundled/upstream/anthropic-skills/skills/claude-api/java/managed-agents/README.md +442 -0
  72. package/bundled/upstream/anthropic-skills/skills/claude-api/php/claude-api.md +10 -10
  73. package/bundled/upstream/anthropic-skills/skills/claude-api/php/managed-agents/README.md +435 -0
  74. package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/README.md +16 -16
  75. package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/batches.md +3 -3
  76. package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/files-api.md +3 -3
  77. package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/streaming.md +7 -7
  78. package/bundled/upstream/anthropic-skills/skills/claude-api/python/claude-api/tool-use.md +19 -19
  79. package/bundled/upstream/anthropic-skills/skills/claude-api/python/managed-agents/README.md +332 -0
  80. package/bundled/upstream/anthropic-skills/skills/claude-api/ruby/claude-api.md +4 -4
  81. package/bundled/upstream/anthropic-skills/skills/claude-api/ruby/managed-agents/README.md +389 -0
  82. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/agent-design.md +101 -0
  83. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/error-codes.md +11 -4
  84. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/live-sources.md +60 -48
  85. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-api-reference.md +372 -0
  86. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-client-patterns.md +209 -0
  87. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-core.md +220 -0
  88. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-environments.md +211 -0
  89. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-events.md +195 -0
  90. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-memory.md +197 -0
  91. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-multiagent.md +99 -0
  92. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-onboarding.md +114 -0
  93. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-outcomes.md +106 -0
  94. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-overview.md +67 -0
  95. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-tools.md +315 -0
  96. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/managed-agents-webhooks.md +110 -0
  97. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/model-migration.md +779 -0
  98. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/models.md +16 -14
  99. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/prompt-caching.md +45 -2
  100. package/bundled/upstream/anthropic-skills/skills/claude-api/shared/tool-use-concepts.md +28 -6
  101. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/README.md +15 -15
  102. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/batches.md +2 -2
  103. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/files-api.md +1 -1
  104. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/streaming.md +5 -5
  105. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/claude-api/tool-use.md +15 -15
  106. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/managed-agents/README.md +359 -0
  107. package/bundled/upstream/anthropic-skills/skills/internal-comms/LICENSE.txt +1 -1
  108. package/bundled/upstream/anthropic-skills/skills/mcp-builder/LICENSE.txt +1 -1
  109. package/bundled/upstream/anthropic-skills/skills/skill-creator/LICENSE.txt +1 -1
  110. package/bundled/upstream/anthropic-skills/skills/slack-gif-creator/LICENSE.txt +1 -1
  111. package/bundled/upstream/anthropic-skills/skills/theme-factory/LICENSE.txt +1 -1
  112. package/bundled/upstream/anthropic-skills/skills/web-artifacts-builder/LICENSE.txt +1 -1
  113. package/bundled/upstream/anthropic-skills/skills/webapp-testing/LICENSE.txt +1 -1
  114. package/bundled/upstream/ecc/.omc-source/bundle.json +2 -3
  115. package/bundled/upstream/ecc/.omc-source/manifests/.claude-plugin/marketplace.json +4 -4
  116. package/bundled/upstream/ecc/agents/a11y-architect.md +10 -2
  117. package/bundled/upstream/ecc/agents/architect.md +9 -0
  118. package/bundled/upstream/ecc/agents/build-error-resolver.md +9 -0
  119. package/bundled/upstream/ecc/agents/chief-of-staff.md +9 -0
  120. package/bundled/upstream/ecc/agents/code-architect.md +9 -0
  121. package/bundled/upstream/ecc/agents/code-explorer.md +10 -1
  122. package/bundled/upstream/ecc/agents/code-reviewer.md +87 -1
  123. package/bundled/upstream/ecc/agents/code-simplifier.md +9 -0
  124. package/bundled/upstream/ecc/agents/comment-analyzer.md +10 -1
  125. package/bundled/upstream/ecc/agents/conversation-analyzer.md +9 -0
  126. package/bundled/upstream/ecc/agents/cpp-build-resolver.md +9 -0
  127. package/bundled/upstream/ecc/agents/cpp-reviewer.md +9 -0
  128. package/bundled/upstream/ecc/agents/csharp-reviewer.md +9 -0
  129. package/bundled/upstream/ecc/agents/dart-build-resolver.md +9 -0
  130. package/bundled/upstream/ecc/agents/database-reviewer.md +9 -0
  131. package/bundled/upstream/ecc/agents/django-build-resolver.md +252 -0
  132. package/bundled/upstream/ecc/agents/django-reviewer.md +169 -0
  133. package/bundled/upstream/ecc/agents/doc-updater.md +9 -0
  134. package/bundled/upstream/ecc/agents/docs-lookup.md +9 -0
  135. package/bundled/upstream/ecc/agents/e2e-runner.md +9 -0
  136. package/bundled/upstream/ecc/agents/fastapi-reviewer.md +79 -0
  137. package/bundled/upstream/ecc/agents/flutter-reviewer.md +9 -0
  138. package/bundled/upstream/ecc/agents/fsharp-reviewer.md +109 -0
  139. package/bundled/upstream/ecc/agents/gan-evaluator.md +9 -0
  140. package/bundled/upstream/ecc/agents/gan-generator.md +9 -0
  141. package/bundled/upstream/ecc/agents/gan-planner.md +9 -0
  142. package/bundled/upstream/ecc/agents/go-build-resolver.md +9 -0
  143. package/bundled/upstream/ecc/agents/go-reviewer.md +9 -0
  144. package/bundled/upstream/ecc/agents/harmonyos-app-resolver.md +182 -0
  145. package/bundled/upstream/ecc/agents/harness-optimizer.md +9 -0
  146. package/bundled/upstream/ecc/agents/healthcare-reviewer.md +9 -0
  147. package/bundled/upstream/ecc/agents/homelab-architect.md +107 -0
  148. package/bundled/upstream/ecc/agents/java-build-resolver.md +133 -11
  149. package/bundled/upstream/ecc/agents/java-reviewer.md +130 -32
  150. package/bundled/upstream/ecc/agents/kotlin-build-resolver.md +9 -0
  151. package/bundled/upstream/ecc/agents/kotlin-reviewer.md +9 -0
  152. package/bundled/upstream/ecc/agents/loop-operator.md +9 -0
  153. package/bundled/upstream/ecc/agents/mle-reviewer.md +162 -0
  154. package/bundled/upstream/ecc/agents/network-architect.md +106 -0
  155. package/bundled/upstream/ecc/agents/network-config-reviewer.md +106 -0
  156. package/bundled/upstream/ecc/agents/network-troubleshooter.md +128 -0
  157. package/bundled/upstream/ecc/agents/opensource-forker.md +9 -0
  158. package/bundled/upstream/ecc/agents/opensource-packager.md +9 -0
  159. package/bundled/upstream/ecc/agents/opensource-sanitizer.md +9 -0
  160. package/bundled/upstream/ecc/agents/performance-optimizer.md +9 -0
  161. package/bundled/upstream/ecc/agents/planner.md +9 -0
  162. package/bundled/upstream/ecc/agents/pr-test-analyzer.md +9 -0
  163. package/bundled/upstream/ecc/agents/python-reviewer.md +9 -0
  164. package/bundled/upstream/ecc/agents/pytorch-build-resolver.md +12 -3
  165. package/bundled/upstream/ecc/agents/refactor-cleaner.md +9 -0
  166. package/bundled/upstream/ecc/agents/rust-build-resolver.md +9 -0
  167. package/bundled/upstream/ecc/agents/rust-reviewer.md +9 -0
  168. package/bundled/upstream/ecc/agents/security-reviewer.md +9 -0
  169. package/bundled/upstream/ecc/agents/seo-specialist.md +10 -1
  170. package/bundled/upstream/ecc/agents/silent-failure-hunter.md +9 -0
  171. package/bundled/upstream/ecc/agents/swift-build-resolver.md +170 -0
  172. package/bundled/upstream/ecc/agents/swift-reviewer.md +116 -0
  173. package/bundled/upstream/ecc/agents/tdd-guide.md +9 -0
  174. package/bundled/upstream/ecc/agents/type-design-analyzer.md +10 -1
  175. package/bundled/upstream/ecc/agents/typescript-reviewer.md +9 -0
  176. package/bundled/upstream/ecc/commands/auto-update.md +28 -0
  177. package/bundled/upstream/ecc/commands/build-fix.md +4 -0
  178. package/bundled/upstream/ecc/commands/checkpoint.md +4 -0
  179. package/bundled/upstream/ecc/commands/code-review.md +3 -3
  180. package/bundled/upstream/ecc/commands/cost-report.md +107 -0
  181. package/bundled/upstream/ecc/commands/cpp-build.md +1 -1
  182. package/bundled/upstream/ecc/commands/cpp-test.md +1 -1
  183. package/bundled/upstream/ecc/commands/ecc-guide.md +93 -0
  184. package/bundled/upstream/ecc/commands/fastapi-review.md +39 -0
  185. package/bundled/upstream/ecc/commands/flutter-build.md +1 -1
  186. package/bundled/upstream/ecc/commands/flutter-test.md +1 -1
  187. package/bundled/upstream/ecc/commands/gan-build.md +4 -0
  188. package/bundled/upstream/ecc/commands/gan-design.md +4 -0
  189. package/bundled/upstream/ecc/commands/go-build.md +1 -1
  190. package/bundled/upstream/ecc/commands/go-test.md +1 -1
  191. package/bundled/upstream/ecc/commands/harness-audit.md +4 -0
  192. package/bundled/upstream/ecc/commands/jira.md +2 -2
  193. package/bundled/upstream/ecc/commands/kotlin-build.md +1 -1
  194. package/bundled/upstream/ecc/commands/kotlin-test.md +1 -1
  195. package/bundled/upstream/ecc/commands/learn.md +4 -0
  196. package/bundled/upstream/ecc/commands/loop-start.md +4 -0
  197. package/bundled/upstream/ecc/commands/loop-status.md +54 -1
  198. package/bundled/upstream/ecc/commands/model-route.md +4 -0
  199. package/bundled/upstream/ecc/commands/multi-backend.md +4 -0
  200. package/bundled/upstream/ecc/commands/multi-execute.md +4 -0
  201. package/bundled/upstream/ecc/commands/multi-frontend.md +4 -0
  202. package/bundled/upstream/ecc/commands/multi-plan.md +4 -0
  203. package/bundled/upstream/ecc/commands/multi-workflow.md +4 -0
  204. package/bundled/upstream/ecc/commands/plan-prd.md +160 -0
  205. package/bundled/upstream/ecc/commands/plan.md +96 -13
  206. package/bundled/upstream/ecc/commands/pm2.md +4 -0
  207. package/bundled/upstream/ecc/commands/pr.md +184 -0
  208. package/bundled/upstream/ecc/commands/project-init.md +86 -0
  209. package/bundled/upstream/ecc/commands/python-review.md +1 -1
  210. package/bundled/upstream/ecc/commands/quality-gate.md +4 -0
  211. package/bundled/upstream/ecc/commands/refactor-clean.md +4 -0
  212. package/bundled/upstream/ecc/commands/rust-build.md +1 -1
  213. package/bundled/upstream/ecc/commands/rust-test.md +1 -1
  214. package/bundled/upstream/ecc/commands/security-scan.md +92 -0
  215. package/bundled/upstream/ecc/commands/sessions.md +6 -6
  216. package/bundled/upstream/ecc/commands/skill-health.md +3 -3
  217. package/bundled/upstream/ecc/commands/test-coverage.md +4 -0
  218. package/bundled/upstream/ecc/commands/update-codemaps.md +4 -0
  219. package/bundled/upstream/ecc/commands/update-docs.md +4 -0
  220. package/bundled/upstream/ecc/skills/accessibility/SKILL.md +1 -1
  221. package/bundled/upstream/ecc/skills/agent-architecture-audit/SKILL.md +256 -0
  222. package/bundled/upstream/ecc/skills/agent-payment-x402/SKILL.md +49 -3
  223. package/bundled/upstream/ecc/skills/agentic-os/SKILL.md +387 -0
  224. package/bundled/upstream/ecc/skills/angular-developer/SKILL.md +154 -0
  225. package/bundled/upstream/ecc/skills/angular-developer/references/angular-animations.md +160 -0
  226. package/bundled/upstream/ecc/skills/angular-developer/references/angular-aria.md +410 -0
  227. package/bundled/upstream/ecc/skills/angular-developer/references/cli.md +86 -0
  228. package/bundled/upstream/ecc/skills/angular-developer/references/component-harnesses.md +59 -0
  229. package/bundled/upstream/ecc/skills/angular-developer/references/component-styling.md +91 -0
  230. package/bundled/upstream/ecc/skills/angular-developer/references/components.md +117 -0
  231. package/bundled/upstream/ecc/skills/angular-developer/references/creating-services.md +97 -0
  232. package/bundled/upstream/ecc/skills/angular-developer/references/data-resolvers.md +69 -0
  233. package/bundled/upstream/ecc/skills/angular-developer/references/define-routes.md +67 -0
  234. package/bundled/upstream/ecc/skills/angular-developer/references/defining-providers.md +72 -0
  235. package/bundled/upstream/ecc/skills/angular-developer/references/di-fundamentals.md +120 -0
  236. package/bundled/upstream/ecc/skills/angular-developer/references/e2e-testing.md +56 -0
  237. package/bundled/upstream/ecc/skills/angular-developer/references/effects.md +83 -0
  238. package/bundled/upstream/ecc/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  239. package/bundled/upstream/ecc/skills/angular-developer/references/host-elements.md +80 -0
  240. package/bundled/upstream/ecc/skills/angular-developer/references/injection-context.md +63 -0
  241. package/bundled/upstream/ecc/skills/angular-developer/references/inputs.md +101 -0
  242. package/bundled/upstream/ecc/skills/angular-developer/references/linked-signal.md +59 -0
  243. package/bundled/upstream/ecc/skills/angular-developer/references/loading-strategies.md +61 -0
  244. package/bundled/upstream/ecc/skills/angular-developer/references/mcp.md +108 -0
  245. package/bundled/upstream/ecc/skills/angular-developer/references/navigate-to-routes.md +69 -0
  246. package/bundled/upstream/ecc/skills/angular-developer/references/outputs.md +86 -0
  247. package/bundled/upstream/ecc/skills/angular-developer/references/reactive-forms.md +122 -0
  248. package/bundled/upstream/ecc/skills/angular-developer/references/rendering-strategies.md +44 -0
  249. package/bundled/upstream/ecc/skills/angular-developer/references/resource.md +77 -0
  250. package/bundled/upstream/ecc/skills/angular-developer/references/route-animations.md +56 -0
  251. package/bundled/upstream/ecc/skills/angular-developer/references/route-guards.md +52 -0
  252. package/bundled/upstream/ecc/skills/angular-developer/references/router-lifecycle.md +45 -0
  253. package/bundled/upstream/ecc/skills/angular-developer/references/router-testing.md +87 -0
  254. package/bundled/upstream/ecc/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  255. package/bundled/upstream/ecc/skills/angular-developer/references/signal-forms.md +795 -0
  256. package/bundled/upstream/ecc/skills/angular-developer/references/signals-overview.md +94 -0
  257. package/bundled/upstream/ecc/skills/angular-developer/references/tailwind-css.md +69 -0
  258. package/bundled/upstream/ecc/skills/angular-developer/references/template-driven-forms.md +114 -0
  259. package/bundled/upstream/ecc/skills/angular-developer/references/testing-fundamentals.md +65 -0
  260. package/bundled/upstream/ecc/skills/autonomous-agent-harness/SKILL.md +6 -0
  261. package/bundled/upstream/ecc/skills/backend-patterns/SKILL.md +8 -45
  262. package/bundled/upstream/ecc/skills/cisco-ios-patterns/SKILL.md +163 -0
  263. package/bundled/upstream/ecc/skills/configure-ecc/SKILL.md +31 -14
  264. package/bundled/upstream/ecc/skills/continuous-learning/SKILL.md +10 -2
  265. package/bundled/upstream/ecc/skills/continuous-learning-v2/SKILL.md +19 -5
  266. package/bundled/upstream/ecc/skills/continuous-learning-v2/agents/observer-loop.sh +44 -4
  267. package/bundled/upstream/ecc/skills/continuous-learning-v2/agents/observer.md +4 -4
  268. package/bundled/upstream/ecc/skills/continuous-learning-v2/agents/start-observer.sh +5 -1
  269. package/bundled/upstream/ecc/skills/continuous-learning-v2/hooks/observe.sh +21 -5
  270. package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/detect-project.sh +58 -13
  271. package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/instinct-cli.py +97 -4
  272. package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/lib/homunculus-dir.sh +31 -0
  273. package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/migrate-homunculus.sh +62 -0
  274. package/bundled/upstream/ecc/skills/continuous-learning-v2/scripts/test_parse_instinct.py +34 -0
  275. package/bundled/upstream/ecc/skills/cost-tracking/SKILL.md +147 -0
  276. package/bundled/upstream/ecc/skills/deep-research/SKILL.md +4 -0
  277. package/bundled/upstream/ecc/skills/defi-amm-security/SKILL.md +6 -0
  278. package/bundled/upstream/ecc/skills/django-celery/SKILL.md +457 -0
  279. package/bundled/upstream/ecc/skills/ecc-guide/SKILL.md +189 -0
  280. package/bundled/upstream/ecc/skills/error-handling/SKILL.md +376 -0
  281. package/bundled/upstream/ecc/skills/exa-search/SKILL.md +4 -0
  282. package/bundled/upstream/ecc/skills/fal-ai-media/SKILL.md +4 -0
  283. package/bundled/upstream/ecc/skills/fastapi-patterns/SKILL.md +327 -0
  284. package/bundled/upstream/ecc/skills/flox-environments/SKILL.md +496 -0
  285. package/bundled/upstream/ecc/skills/frontend-design-direction/SKILL.md +92 -0
  286. package/bundled/upstream/ecc/skills/frontend-slides/animation-patterns.md +122 -0
  287. package/bundled/upstream/ecc/skills/frontend-slides/html-template.md +419 -0
  288. package/bundled/upstream/ecc/skills/frontend-slides/scripts/export-pdf.sh +418 -0
  289. package/bundled/upstream/ecc/skills/frontend-slides/scripts/extract-pptx.py +96 -0
  290. package/bundled/upstream/ecc/skills/frontend-slides/viewport-base.css +153 -0
  291. package/bundled/upstream/ecc/skills/fsharp-testing/SKILL.md +280 -0
  292. package/bundled/upstream/ecc/skills/gateguard/SKILL.md +4 -0
  293. package/bundled/upstream/ecc/skills/hermes-imports/SKILL.md +88 -0
  294. package/bundled/upstream/ecc/skills/homelab-network-readiness/SKILL.md +169 -0
  295. package/bundled/upstream/ecc/skills/homelab-network-setup/SKILL.md +129 -0
  296. package/bundled/upstream/ecc/skills/homelab-pihole-dns/SKILL.md +274 -0
  297. package/bundled/upstream/ecc/skills/homelab-vlan-segmentation/SKILL.md +311 -0
  298. package/bundled/upstream/ecc/skills/homelab-wireguard-vpn/SKILL.md +305 -0
  299. package/bundled/upstream/ecc/skills/ios-icon-gen/SKILL.md +157 -0
  300. package/bundled/upstream/ecc/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
  301. package/bundled/upstream/ecc/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
  302. package/bundled/upstream/ecc/skills/java-coding-standards/SKILL.md +241 -5
  303. package/bundled/upstream/ecc/skills/make-interfaces-feel-better/SKILL.md +151 -0
  304. package/bundled/upstream/ecc/skills/mle-workflow/SKILL.md +346 -0
  305. package/bundled/upstream/ecc/skills/motion-advanced/SKILL.md +596 -0
  306. package/bundled/upstream/ecc/skills/motion-foundations/SKILL.md +299 -0
  307. package/bundled/upstream/ecc/skills/motion-patterns/SKILL.md +435 -0
  308. package/bundled/upstream/ecc/skills/motion-ui/SKILL.md +575 -0
  309. package/bundled/upstream/ecc/skills/mysql-patterns/SKILL.md +412 -0
  310. package/bundled/upstream/ecc/skills/netmiko-ssh-automation/SKILL.md +173 -0
  311. package/bundled/upstream/ecc/skills/network-bgp-diagnostics/SKILL.md +167 -0
  312. package/bundled/upstream/ecc/skills/network-config-validation/SKILL.md +210 -0
  313. package/bundled/upstream/ecc/skills/network-interface-health/SKILL.md +152 -0
  314. package/bundled/upstream/ecc/skills/openclaw-persona-forge/SKILL.md +1 -9
  315. package/bundled/upstream/ecc/skills/plan-orchestrate/SKILL.md +262 -0
  316. package/bundled/upstream/ecc/skills/prisma-patterns/SKILL.md +371 -0
  317. package/bundled/upstream/ecc/skills/production-audit/SKILL.md +206 -0
  318. package/bundled/upstream/ecc/skills/prompt-optimizer/SKILL.md +24 -400
  319. package/bundled/upstream/ecc/skills/quarkus-patterns/SKILL.md +722 -0
  320. package/bundled/upstream/ecc/skills/quarkus-security/SKILL.md +467 -0
  321. package/bundled/upstream/ecc/skills/quarkus-tdd/SKILL.md +811 -0
  322. package/bundled/upstream/ecc/skills/quarkus-verification/SKILL.md +479 -0
  323. package/bundled/upstream/ecc/skills/redis-patterns/SKILL.md +403 -0
  324. package/bundled/upstream/ecc/skills/scientific-db-pubmed-database/SKILL.md +175 -0
  325. package/bundled/upstream/ecc/skills/scientific-db-uspto-database/SKILL.md +177 -0
  326. package/bundled/upstream/ecc/skills/scientific-pkg-gget/SKILL.md +166 -0
  327. package/bundled/upstream/ecc/skills/scientific-thinking-literature-review/SKILL.md +192 -0
  328. package/bundled/upstream/ecc/skills/scientific-thinking-scholar-evaluation/SKILL.md +160 -0
  329. package/bundled/upstream/ecc/skills/search-first/SKILL.md +23 -2
  330. package/bundled/upstream/ecc/skills/security-review/SKILL.md +10 -2
  331. package/bundled/upstream/ecc/skills/skill-comply/scripts/runner.py +28 -3
  332. package/bundled/upstream/ecc/skills/skill-comply/tests/test_runner.py +172 -0
  333. package/bundled/upstream/ecc/skills/skill-scout/SKILL.md +140 -0
  334. package/bundled/upstream/ecc/skills/skill-stocktake/SKILL.md +1 -0
  335. package/bundled/upstream/ecc/skills/strategic-compact/SKILL.md +2 -2
  336. package/bundled/upstream/ecc/skills/tinystruct-patterns/SKILL.md +203 -0
  337. package/bundled/upstream/ecc/skills/tinystruct-patterns/references/architecture.md +90 -0
  338. package/bundled/upstream/ecc/skills/tinystruct-patterns/references/data-handling.md +60 -0
  339. package/bundled/upstream/ecc/skills/tinystruct-patterns/references/database.md +99 -0
  340. package/bundled/upstream/ecc/skills/tinystruct-patterns/references/routing.md +64 -0
  341. package/bundled/upstream/ecc/skills/tinystruct-patterns/references/system-usage.md +97 -0
  342. package/bundled/upstream/ecc/skills/tinystruct-patterns/references/testing.md +72 -0
  343. package/bundled/upstream/ecc/skills/ui-to-vue/SKILL.md +134 -0
  344. package/bundled/upstream/ecc/skills/vite-patterns/SKILL.md +449 -0
  345. package/bundled/upstream/ecc/skills/windows-desktop-e2e/SKILL.md +788 -0
  346. package/bundled/upstream/ecc/skills/x-api/SKILL.md +4 -0
  347. package/bundled/upstream/oh-my-claudecode/.omc-source/bundle.json +20 -0
  348. package/bundled/upstream/oh-my-claudecode/agents/analyst.md +2 -1
  349. package/bundled/upstream/oh-my-claudecode/agents/architect.md +2 -1
  350. package/bundled/upstream/oh-my-claudecode/agents/code-reviewer.md +26 -7
  351. package/bundled/upstream/oh-my-claudecode/agents/critic.md +2 -1
  352. package/bundled/upstream/oh-my-claudecode/agents/debugger.md +2 -1
  353. package/bundled/upstream/oh-my-claudecode/agents/designer.md +14 -1
  354. package/bundled/upstream/oh-my-claudecode/agents/document-specialist.md +1 -1
  355. package/bundled/upstream/oh-my-claudecode/agents/executor.md +2 -1
  356. package/bundled/upstream/oh-my-claudecode/agents/explore.md +2 -1
  357. package/bundled/upstream/oh-my-claudecode/agents/git-master.md +2 -1
  358. package/bundled/upstream/oh-my-claudecode/agents/planner.md +2 -1
  359. package/bundled/upstream/oh-my-claudecode/agents/qa-tester.md +2 -1
  360. package/bundled/upstream/oh-my-claudecode/agents/scientist.md +2 -1
  361. package/bundled/upstream/oh-my-claudecode/agents/security-reviewer.md +2 -1
  362. package/bundled/upstream/oh-my-claudecode/agents/test-engineer.md +2 -1
  363. package/bundled/upstream/oh-my-claudecode/agents/tracer.md +2 -1
  364. package/bundled/upstream/oh-my-claudecode/agents/verifier.md +2 -1
  365. package/bundled/upstream/oh-my-claudecode/agents/writer.md +2 -1
  366. package/bundled/upstream/oh-my-claudecode/skills/AGENTS.md +4 -3
  367. package/bundled/upstream/oh-my-claudecode/skills/ai-slop-cleaner/SKILL.md +12 -0
  368. package/bundled/upstream/oh-my-claudecode/skills/deep-dive/SKILL.md +69 -13
  369. package/bundled/upstream/oh-my-claudecode/skills/deep-interview/SKILL.md +176 -63
  370. package/bundled/upstream/oh-my-claudecode/skills/learner/SKILL.md +3 -1
  371. package/bundled/upstream/oh-my-claudecode/skills/omc-doctor/SKILL.md +22 -3
  372. package/bundled/upstream/oh-my-claudecode/skills/omc-reference/SKILL.md +1 -1
  373. package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/01-install-claude-md.md +2 -2
  374. package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/02-configure.md +45 -2
  375. package/bundled/upstream/oh-my-claudecode/skills/omc-setup/phases/03-integrations.md +46 -5
  376. package/bundled/upstream/oh-my-claudecode/skills/plan/SKILL.md +19 -17
  377. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/config.sh +1 -1
  378. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/parse.sh +1 -1
  379. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/azure-devops.sh +1 -1
  380. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/bitbucket.sh +1 -1
  381. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/gitea.sh +1 -1
  382. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/github.sh +1 -1
  383. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/gitlab.sh +1 -1
  384. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/interface.sh +1 -1
  385. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/providers/jira.sh +1 -1
  386. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/session.sh +1 -1
  387. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/tmux.sh +1 -1
  388. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/lib/worktree.sh +1 -1
  389. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/psm.sh +1 -1
  390. package/bundled/upstream/oh-my-claudecode/skills/project-session-manager/tests/test-psm-prompt-injection.sh +1 -1
  391. package/bundled/upstream/oh-my-claudecode/skills/ralph/SKILL.md +8 -8
  392. package/bundled/upstream/oh-my-claudecode/skills/ralplan/SKILL.md +9 -5
  393. package/bundled/upstream/oh-my-claudecode/skills/skill/SKILL.md +6 -6
  394. package/bundled/upstream/oh-my-claudecode/skills/skillify/SKILL.md +19 -3
  395. package/bundled/upstream/oh-my-claudecode/skills/trace/SKILL.md +3 -1
  396. package/bundled/upstream/oh-my-claudecode/skills/ultrawork/SKILL.md +20 -7
  397. package/bundled/upstream/superpowers/.omc-source/bundle.json +21 -0
  398. package/bundled/upstream/superpowers/hooks/hooks-cursor.json +1 -1
  399. package/bundled/upstream/superpowers/skills/executing-plans/SKILL.md +1 -1
  400. package/bundled/upstream/superpowers/skills/finishing-a-development-branch/SKILL.md +93 -42
  401. package/bundled/upstream/superpowers/skills/requesting-code-review/SKILL.md +7 -9
  402. package/bundled/upstream/superpowers/skills/requesting-code-review/code-reviewer.md +107 -85
  403. package/bundled/upstream/superpowers/skills/subagent-driven-development/SKILL.md +3 -1
  404. package/bundled/upstream/superpowers/skills/subagent-driven-development/code-quality-reviewer-prompt.md +2 -3
  405. package/bundled/upstream/superpowers/skills/systematic-debugging/CREATION-LOG.md +1 -1
  406. package/bundled/upstream/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
  407. package/bundled/upstream/superpowers/skills/using-git-worktrees/SKILL.md +95 -98
  408. package/bundled/upstream/superpowers/skills/using-superpowers/references/codex-tools.md +7 -48
  409. package/bundled/upstream/superpowers/skills/using-superpowers/references/copilot-tools.md +1 -11
  410. package/bundled/upstream/superpowers/skills/using-superpowers/references/gemini-tools.md +21 -3
  411. package/bundled/upstream/superpowers/skills/writing-plans/SKILL.md +1 -1
  412. package/package.json +1 -1
  413. package/bundled/upstream/anthropic-skills/skills/claude-api/python/agent-sdk/README.md +0 -355
  414. package/bundled/upstream/anthropic-skills/skills/claude-api/python/agent-sdk/patterns.md +0 -359
  415. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/agent-sdk/README.md +0 -297
  416. package/bundled/upstream/anthropic-skills/skills/claude-api/typescript/agent-sdk/patterns.md +0 -209
  417. package/bundled/upstream/ecc/commands/agent-sort.md +0 -23
  418. package/bundled/upstream/ecc/commands/claw.md +0 -23
  419. package/bundled/upstream/ecc/commands/context-budget.md +0 -23
  420. package/bundled/upstream/ecc/commands/devfleet.md +0 -23
  421. package/bundled/upstream/ecc/commands/docs.md +0 -23
  422. package/bundled/upstream/ecc/commands/e2e.md +0 -268
  423. package/bundled/upstream/ecc/commands/eval.md +0 -23
  424. package/bundled/upstream/ecc/commands/orchestrate.md +0 -135
  425. package/bundled/upstream/ecc/commands/prompt-optimize.md +0 -23
  426. package/bundled/upstream/ecc/commands/rules-distill.md +0 -20
  427. package/bundled/upstream/ecc/commands/tdd.md +0 -231
  428. package/bundled/upstream/ecc/commands/verify.md +0 -23
  429. package/bundled/upstream/ecc/skills/claude-api/SKILL.md +0 -337
  430. package/bundled/upstream/ecc/skills/frontend-design/SKILL.md +0 -145
  431. package/bundled/upstream/superpowers/agents/code-reviewer.md +0 -48
  432. package/bundled/upstream/superpowers/commands/brainstorm.md +0 -5
  433. package/bundled/upstream/superpowers/commands/execute-plan.md +0 -5
  434. package/bundled/upstream/superpowers/commands/write-plan.md +0 -5
@@ -0,0 +1,1093 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * evaluate-convergence.mjs β€” Stage 5: THE sole executable convergence authority
4
+ *
5
+ * This script is the ONLY thing that may emit `convergence/<component>.json`.
6
+ * The LLM orchestrator NEVER hand-writes that artifact β€” it calls this script,
7
+ * which mechanically decides the verdict and EXITS NON-ZERO on any guard
8
+ * violation so a pipeline cannot ignore a failed gate.
9
+ *
10
+ * Trust model (honest β€” see spec.md Β§1.1: report the residual, do not pretend
11
+ * it is zero β€” and apply that to the skill's OWN trust model):
12
+ * This grader is mechanically AUTHORITATIVE *given* inputs whose provenance
13
+ * it now binds in code. Each bullet states ONLY what is mechanically true β€”
14
+ * the precise bound/residual split, not an over-claim:
15
+ * - the structured calibration GATE is recomputed from `calib.floor`
16
+ * using the published tolerances; a hand-loosened `gate` (tight floor +
17
+ * loose gate) is rejected (`gate-floor-mismatch`). This binds the gate
18
+ * TO the floor β€” it does not bind the floor's *value*;
19
+ * - `calibration_source` binds the IDENTITY of the bundled twin SOURCE
20
+ * FILES (excluding build output/dotfiles): their source-tree hashes are
21
+ * recomputed here from the actual
22
+ * `assets/calibration/{h5-twin,swiftui-twin}` (mismatch β‡’
23
+ * `calibration-twin-mismatch`). This does NOT re-measure or bind the
24
+ * `floor` *value* itself;
25
+ * - the `floor` VALUE is asserted to satisfy calibrate-render.mjs's OWN
26
+ * sanity envelope (shared `_calib-consts.mjs`: ssim_nontext β‰₯ 0.95,
27
+ * non-null text_iou β‰₯ 0.9, metric-valid deltaE) β€” a floor calibrate-
28
+ * render could not have emitted (it writes blocked.json below this) is
29
+ * rejected `floor-implausible` (exit 1). The grader does NOT re-render
30
+ * to re-measure the floor: a floor *within* this envelope yet looser
31
+ * than the true measured floor is trusted (named residual (2) below);
32
+ * - the judge negative control is bound to the shipped, hash-pinned
33
+ * `assets/calibration/swiftui-twin-divergent` SOURCE files (mismatch /
34
+ * unstructured / not-rejected β‡’ any YES is VOID,
35
+ * `negative-control-unbound`);
36
+ * - pHash never short-circuits; null IoU is FAIL; the mask budget is
37
+ * monotone-or-fail; sim-screenshot.sh carries its own no-fake spine.
38
+ * NAMED IRREDUCIBLE RESIDUALS (BOTH stated, neither hidden β€” a fully
39
+ * zero-trust verdict is impossible because something must run the renders
40
+ * and something must measure the floor):
41
+ * (1) The grader CANNOT re-execute the simulator renders. It trusts the
42
+ * per-iteration pixel-diff JSONs were produced by running the real
43
+ * `pixel-diff.mjs` on real `sim-screenshot.sh` renders (bounded by
44
+ * that script's no-fake build/env spine β€” no simulator / no build β‡’
45
+ * blocked/needs-human, never converged).
46
+ * (2) The grader CANNOT re-measure the calibration floor. It asserts the
47
+ * supplied `floor` satisfies calibrate-render.mjs's own sanity
48
+ * envelope and recomputes the gate from it, but a `floor` *within*
49
+ * that envelope yet looser than the TRUE measured floor is trusted
50
+ * (it cannot re-render the bundled twins to re-derive the real
51
+ * number). Mitigated by: the orchestrator's contractual obligation
52
+ * to run the real, sanity/flat-image-spined `calibrate-render.mjs`,
53
+ * and the human-readable `calibration_provenance` recorded in the
54
+ * convergence artifact.
55
+ * These are the deliberate, documented boundaries β€” the deliverable is
56
+ * "maximally provenance-bound + honestly disclosed residual", explicitly
57
+ * NOT "zero-trust". A reviewer reading this header learns BOTH residuals.
58
+ *
59
+ * Usage:
60
+ * node evaluate-convergence.mjs --iterations iterations.json
61
+ * --calibration calibration.json
62
+ * --judge judge.json
63
+ * --component <Name>
64
+ * [--masks masks.json]
65
+ * [--component-area <WxH | px>]
66
+ * [--out convergence/<Name>.json]
67
+ * [--blocked blocked.json]
68
+ *
69
+ * Inputs:
70
+ * --iterations <path> REQUIRED. Either:
71
+ * (a) a JSON file: [ { "i":1, "diff_json_path":"...", "built":true }, ... ]
72
+ * where `diff_json_path` points at a pixel-diff.mjs JSON output and
73
+ * `built` is the caller-supplied build result for that iteration; OR
74
+ * (b) a directory: every *.json inside is treated as a pixel-diff output;
75
+ * iteration index = numeric suffix (iterN) or file order; `built`
76
+ * defaults to true ONLY in directory mode (use form (a) to record
77
+ * non-building iterations).
78
+ * The caller supplies `built`; the caller does NOT (and cannot) supply
79
+ * `gate_passed` β€” this script computes it.
80
+ * --calibration <path> REQUIRED. calibrate-render.mjs output. Consumes the
81
+ * STRUCTURED numeric gate (gate.converged / gate.close objects), never a
82
+ * string DSL. A legacy string gate is rejected as un-enforceable.
83
+ * schema MUST be "h5-to-swiftui/calibration@1"; `gate` is REJECTED
84
+ * unless it equals the gate recomputed from `floor` with the published
85
+ * tolerances (reason: gate-floor-mismatch); `calibration_source` MUST
86
+ * match the SHIPPED bundled twins' source-tree hashes recomputed here
87
+ * (reason: calibration-twin-mismatch).
88
+ * --judge <path> REQUIRED. {
89
+ * "negative_control": {
90
+ * "stimulus_source_hash": "<sha256 source-tree hash of the
91
+ * bundled assets/calibration/swiftui-twin-divergent>",
92
+ * "divergent_pair": "h5-twin vs swiftui-twin-divergent",
93
+ * "rejected": true,
94
+ * "differences": [ { "desc": "...", "severity": "..." }, ... ]
95
+ * },
96
+ * "framing": "forced-difference-3",
97
+ * "differences": [ { "desc": "...", "severity": "..." }, ... ],
98
+ * "verdict": "YES" | "NO" | "visually-equivalent-residual-subperceptual"
99
+ * }
100
+ * The legacy bare `negative_control:"passed"` STRING form is REJECTED
101
+ * as unbound (it asserts a judge run nothing verified) β‡’ any YES VOID.
102
+ * --masks <path> Optional. Array of { x, y, w, h, reason }
103
+ * (component-relative px). Every mask MUST have a non-empty `reason`.
104
+ * --component-area Optional. "WxH" (e.g. "320x140") or a raw pixel
105
+ * integer. Used as the mask-fraction denominator. If omitted, the area
106
+ * is taken from the first diff's region coverage / global frame if
107
+ * derivable; if it cannot be derived AND masks are present, that is a
108
+ * guard violation (cannot verify the mask budget) β‡’ needs-human.
109
+ * --blocked <path> Optional. If this file exists, the component is
110
+ * `blocked` regardless of metrics (never converged).
111
+ *
112
+ * Mechanically enforced guards (each violation β‡’ tier downgrade + non-zero
113
+ * exit, with a machine-readable reason β€” NEVER a silent pass):
114
+ * 1. Mask budget: sum(mask area)/component area > 0.10 β‡’ needs-human.
115
+ * Any mask missing a non-empty `reason` β‡’ reject (needs-human).
116
+ * 2. Gate eval in code: read calibration structured thresholds; compute
117
+ * `gate_passed` PER ITERATION from that iteration's pixel-diff metrics.
118
+ * A text-region IoU of `null` counts as FAIL (never pass). pHash is
119
+ * necessary-not-sufficient β€” phash_fast_candidate never short-circuits
120
+ * to converged; the region gate must still pass.
121
+ * 3. Monotone-or-fail best-of-N: best_iteration is THIS script's choice,
122
+ * only among iterations that are BOTH built==true AND gate_passed==true.
123
+ * Caller-supplied best_iteration is ignored. None β‡’ needs-human.
124
+ * 4. Negative control voids judge β€” BOUND to the bundled divergent twin:
125
+ * judge.negative_control MUST be a structured object whose
126
+ * stimulus_source_hash equals the source-tree hash of the SHIPPED
127
+ * assets/calibration/swiftui-twin-divergent (recomputed here),
128
+ * rejected===true, differences a non-empty structured list, AND
129
+ * judge.framing === "forced-difference-3". The legacy bare string form
130
+ * is rejected as unbound. Any deviation β‡’ any YES VOID; cannot be
131
+ * converged (needs-human), recorded negative_control:failed,
132
+ * reason: negative-control-unbound.
133
+ * 5. Tiered verdict (visual-diff-loop-protocol.md):
134
+ * converged = gate_passed(best) AND judge YES (valid neg-control);
135
+ * close = within the calibration `close` band AND judge
136
+ * "visually-equivalent-residual-subperceptual";
137
+ * else needs-human.
138
+ * 6. Build accounting: no built+gate-passing iteration, or a blocked.json
139
+ * present β‡’ needs-human / blocked, NEVER converged.
140
+ *
141
+ * Output: writes the `h5-to-swiftui/convergence@1` artifact (pinned-version
142
+ * header pass-through from calibration, full per-iteration history, masks,
143
+ * mask_fraction, negative_control result, best_iteration, tier, residual).
144
+ *
145
+ * Exit codes:
146
+ * 0 β€” tier is `converged` OR `close` (pipeline may proceed)
147
+ * 1 β€” fatal error (bad args, file not found, malformed input,
148
+ * legacy string gate, unverifiable mask budget)
149
+ * 3 β€” tier is `needs-human` OR a guard was violated (mask budget,
150
+ * negative control, no gate-passing iteration)
151
+ * 4 β€” tier is `blocked` (blocked.json present for this component)
152
+ * (--help exits 0)
153
+ *
154
+ * No npm dependencies β€” pure JSON; pngjs is NOT required.
155
+ *
156
+ * Examples:
157
+ * node evaluate-convergence.mjs --iterations iters.json \
158
+ * --calibration calibration.json --judge judge.json --component ProductCard
159
+ * node evaluate-convergence.mjs --iterations .h5-to-swiftui/diff/ProductCard \
160
+ * --calibration calibration.json --judge judge.json --masks masks.json \
161
+ * --component-area 320x140 --component ProductCard \
162
+ * --out .h5-to-swiftui/convergence/ProductCard.json
163
+ */
164
+
165
+ import {
166
+ existsSync,
167
+ readFileSync,
168
+ writeFileSync,
169
+ mkdirSync,
170
+ readdirSync,
171
+ statSync,
172
+ } from 'node:fs';
173
+ import { resolve, join, dirname, basename } from 'node:path';
174
+ import { fileURLToPath } from 'node:url';
175
+
176
+ import { sourceTreeHash } from './_provenance.mjs';
177
+ // Shared sanity-envelope constants β€” the SAME module calibrate-render.mjs
178
+ // imports, so a `floor` this grader accepts is one calibrate-render could
179
+ // actually have emitted (vs writing blocked.json). Single source of truth.
180
+ import { floorWithinCalibrateEnvelope } from './_calib-consts.mjs';
181
+
182
+ // Skill root = parent of this script's directory (scripts/ -> skill root),
183
+ // resolved from the script's own location so bundled-asset provenance is
184
+ // correct no matter what cwd the grader runs from.
185
+ const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
186
+ const BUNDLED_H5_TWIN = join(SKILL_ROOT, 'assets/calibration/h5-twin');
187
+ const BUNDLED_SWIFTUI_TWIN = join(SKILL_ROOT, 'assets/calibration/swiftui-twin');
188
+ const BUNDLED_DIVERGENT_TWIN = join(SKILL_ROOT, 'assets/calibration/swiftui-twin-divergent');
189
+
190
+ // ── CLI ───────────────────────────────────────────────────────────────────────
191
+
192
+ const args = process.argv.slice(2);
193
+
194
+ if (args.includes('--help') || args.includes('-h')) {
195
+ console.log(`evaluate-convergence.mjs β€” Stage 5 sole executable convergence authority
196
+
197
+ Usage:
198
+ node evaluate-convergence.mjs --iterations <iters.json | dir>
199
+ --calibration <calibration.json>
200
+ --judge <judge.json>
201
+ --component <Name>
202
+ [--masks <masks.json>]
203
+ [--component-area <WxH | px>]
204
+ [--out <convergence/Name.json>]
205
+ [--blocked <blocked.json>]
206
+
207
+ Required:
208
+ --iterations <path> iterations.json [{i,diff_json_path,built}, ...] OR a
209
+ directory of pixel-diff.mjs JSON outputs. The caller
210
+ supplies 'built'; this script computes 'gate_passed'.
211
+ --calibration <path> calibrate-render.mjs output. STRUCTURED numeric gate
212
+ (legacy string gate rejected). schema must be
213
+ h5-to-swiftui/calibration@1; gate must equal the
214
+ gate recomputed from floor (gate-floor-mismatch);
215
+ calibration_source must match the bundled twins'
216
+ source-tree hashes (calibration-twin-mismatch).
217
+ --judge <path> { negative_control:{stimulus_source_hash,
218
+ divergent_pair,rejected,differences}, framing,
219
+ differences, verdict }. The legacy bare
220
+ negative_control:"passed" string is rejected
221
+ (unbound) β‡’ any YES VOID.
222
+ --component <Name> Component name (artifact + best-of-N scope)
223
+
224
+ Optional:
225
+ --masks <path> [{x,y,w,h,reason}]; every mask needs a non-empty reason
226
+ --component-area "WxH" or px integer; mask-fraction denominator
227
+ --out <path> Where to write convergence/<Name>.json
228
+ (default: stdout only)
229
+ --blocked <path> If present, component is 'blocked' (never converged)
230
+
231
+ Mechanically enforced guards (violation β‡’ downgrade + non-zero exit):
232
+ 0 provenance: schema ok; gate == recompute(floor); calibration_source ==
233
+ bundled-twin source-tree hashes (else exit 1 gate-floor-mismatch /
234
+ calibration-twin-mismatch)
235
+ 1 mask budget > 10% β‡’ needs-human; mask w/o reason β‡’ reject
236
+ 2 gate eval in code, per iteration; iou null = FAIL; pHash never converges
237
+ 3 monotone-or-fail best-of-N: built && gate_passed only; none β‡’ needs-human
238
+ 4 negative control BOUND to bundled swiftui-twin-divergent (structured
239
+ {stimulus_source_hash,rejected,differences} + forced-difference-3);
240
+ unbound β‡’ any YES VOID, needs-human, negative-control-unbound
241
+ 5 tiered verdict per visual-diff-loop-protocol.md
242
+ 6 blocked.json present / no built+passing iter β‡’ never converged
243
+
244
+ Exit codes:
245
+ 0 converged | close (pipeline may proceed)
246
+ 1 fatal error (bad input, legacy string gate, schema mismatch,
247
+ gate-floor-mismatch, calibration-twin-mismatch, unverifiable mask)
248
+ 3 needs-human | guard violation
249
+ 4 blocked
250
+ (--help exits 0)
251
+
252
+ No npm deps (pure JSON; pngjs NOT required).
253
+
254
+ Examples:
255
+ node evaluate-convergence.mjs --iterations iters.json \\
256
+ --calibration calibration.json --judge judge.json --component ProductCard
257
+ node evaluate-convergence.mjs --iterations .h5-to-swiftui/diff/ProductCard \\
258
+ --calibration calibration.json --judge judge.json --masks masks.json \\
259
+ --component-area 320x140 --component ProductCard \\
260
+ --out .h5-to-swiftui/convergence/ProductCard.json
261
+ `);
262
+ process.exit(0);
263
+ }
264
+
265
+ function getFlag(flag, fallback = null) {
266
+ const idx = args.indexOf(flag);
267
+ if (idx === -1) return fallback;
268
+ const val = args[idx + 1];
269
+ if (val === undefined || val.startsWith('--')) {
270
+ console.error(`Error: ${flag} requires an argument.`);
271
+ process.exit(1);
272
+ }
273
+ return val;
274
+ }
275
+
276
+ const iterationsArg = getFlag('--iterations');
277
+ const calibArg = getFlag('--calibration');
278
+ const judgeArg = getFlag('--judge');
279
+ const componentName = getFlag('--component');
280
+ const masksArg = getFlag('--masks');
281
+ const areaArg = getFlag('--component-area');
282
+ const outArg = getFlag('--out');
283
+ const blockedArg = getFlag('--blocked');
284
+
285
+ function fatal(msg) {
286
+ console.error(`Error: ${msg}\nRun with --help for usage.`);
287
+ process.exit(1);
288
+ }
289
+
290
+ if (!iterationsArg) fatal('--iterations is required.');
291
+ if (!calibArg) fatal('--calibration is required.');
292
+ if (!judgeArg) fatal('--judge is required.');
293
+ if (!componentName) fatal('--component is required.');
294
+
295
+ const iterationsPath = resolve(iterationsArg);
296
+ const calibPath = resolve(calibArg);
297
+ const judgePath = resolve(judgeArg);
298
+ const masksPath = masksArg ? resolve(masksArg) : null;
299
+ const blockedPath = blockedArg ? resolve(blockedArg) : null;
300
+
301
+ if (!existsSync(iterationsPath)) fatal(`--iterations not found: ${iterationsPath}`);
302
+ if (!existsSync(calibPath)) fatal(`--calibration not found: ${calibPath}`);
303
+ if (!existsSync(judgePath)) fatal(`--judge not found: ${judgePath}`);
304
+ if (masksPath && !existsSync(masksPath)) fatal(`--masks not found: ${masksPath}`);
305
+
306
+ function readJson(p, label) {
307
+ try {
308
+ return JSON.parse(readFileSync(p, 'utf8'));
309
+ } catch (e) {
310
+ fatal(`cannot parse ${label} (${p}): ${e.message}`);
311
+ }
312
+ }
313
+
314
+ // ── Load calibration + validate STRUCTURED gate ───────────────────────────────
315
+
316
+ const calib = readJson(calibPath, '--calibration');
317
+ const gate = calib?.gate;
318
+
319
+ if (!gate || typeof gate !== 'object') {
320
+ fatal('calibration.json has no `gate` object.');
321
+ }
322
+ // Reject the legacy string DSL β€” it is not mechanically enforceable here.
323
+ if (typeof gate.converged === 'string' || typeof gate.close === 'string') {
324
+ fatal(
325
+ 'calibration.json `gate.converged`/`gate.close` is a legacy STRING DSL. ' +
326
+ 'evaluate-convergence.mjs requires the STRUCTURED numeric gate emitted by ' +
327
+ 'the current calibrate-render.mjs (objects with ssim_nontext_min, ' +
328
+ 'deltaE_p95_max, text_iou_min). Re-run calibration to regenerate.'
329
+ );
330
+ }
331
+ const gateConverged = gate.converged;
332
+ const gateClose = gate.close;
333
+ if (!gateConverged || typeof gateConverged !== 'object'
334
+ || !gateClose || typeof gateClose !== 'object') {
335
+ fatal('calibration.json `gate.converged`/`gate.close` must be numeric objects.');
336
+ }
337
+ for (const [name, g] of [['converged', gateConverged], ['close', gateClose]]) {
338
+ if (typeof g.ssim_nontext_min !== 'number'
339
+ || typeof g.deltaE_p95_max !== 'number') {
340
+ fatal(`gate.${name} missing numeric ssim_nontext_min / deltaE_p95_max.`);
341
+ }
342
+ }
343
+
344
+ // ── FIX 1: provenance β€” schema + gate-recomputed-from-floor ───────────────────
345
+ // A loose `gate` (tight-looking floor + hand-loosened gate) is one forge
346
+ // path. Close it: assert the calibration schema, then RECOMPUTE the
347
+ // expected structured gate from `calib.floor` using the SAME published
348
+ // tolerances calibrate-render.mjs uses, and reject any deviation beyond a
349
+ // tiny rounding epsilon. This binds the GATE to the floor: a hand-loosened
350
+ // gate is rejected unless the FLOOR itself is loosened. FIX 2 binds the
351
+ // twin SOURCE IDENTITY (not the floor value); FIX A asserts the floor value
352
+ // is within calibrate-render's own sanity envelope. A floor *within* that
353
+ // envelope but looser than the true measured floor remains trusted
354
+ // (residual (2), disclosed in the Trust-model header).
355
+
356
+ if (calib?.schema !== 'h5-to-swiftui/calibration@1') {
357
+ fatal(
358
+ `calibration.json schema is "${calib?.schema ?? 'absent'}" β€” expected ` +
359
+ '"h5-to-swiftui/calibration@1". Refusing to grade against an ' +
360
+ 'unrecognized calibration contract.'
361
+ );
362
+ }
363
+
364
+ const floor = calib?.floor;
365
+ if (!floor || typeof floor !== 'object'
366
+ || typeof floor.ssim_nontext !== 'number'
367
+ || typeof floor.deltaE_p95 !== 'number'
368
+ || !(floor.text_iou === null || typeof floor.text_iou === 'number')) {
369
+ fatal(
370
+ 'calibration.json `floor` must carry numeric ssim_nontext + deltaE_p95 ' +
371
+ 'and a numeric|null text_iou β€” cannot recompute the expected gate from ' +
372
+ 'an unverifiable floor.'
373
+ );
374
+ }
375
+
376
+ // ── FIX A: floor must satisfy calibrate-render's OWN sanity envelope ──────────
377
+ // `calibration_source` (FIX 2) binds the IDENTITY of the bundled twin source
378
+ // files but NOT the MEASURED floor values. Without this check an attacker
379
+ // could write an absurdly loose floor (e.g. ssim_nontext:0.05,
380
+ // deltaE_p95:200), copy the REAL public bundled-twin source hashes into
381
+ // `calibration_source`, let the gate recompute consistently from that absurd
382
+ // floor, and grade visually-broken output as `converged` with ZERO shipped
383
+ // files altered.
384
+ //
385
+ // calibrate-render.mjs ALREADY refuses to EMIT a floor that fails its sanity
386
+ // bound β€” it writes blocked.json instead of calibration.json. Therefore a
387
+ // calibration.json whose `floor` violates that SAME bound could not have been
388
+ // produced by an honest calibrate-render.mjs run. We reject it on consistency
389
+ // grounds. The envelope constants (SSIM_NONTEXT_MIN=0.95, TEXT_IOU_MIN=0.9)
390
+ // come from ./_calib-consts.mjs β€” the single source of truth calibrate-render
391
+ // also imports β€” so this assertion is provably the same bound the producer
392
+ // enforces; no new lenient constant is invented (see _calib-consts.mjs for
393
+ // the per-bound provenance: calibrate-render lines ~430/431 +
394
+ // render-equivalence-calibration.md Β§"Sanity bound" + findings RQ4).
395
+ //
396
+ // This check runs BEFORE the gate-from-floor recompute below, so a fabricated
397
+ // floor is rejected before any gate is derived from it. It does NOT close the
398
+ // residual entirely: a floor *within* this envelope yet looser than the true
399
+ // measured floor is still trusted (the grader cannot re-render to re-measure
400
+ // it) β€” that residual is disclosed honestly in the Trust-model header above
401
+ // and in references/*.md / SKILL.md.
402
+ {
403
+ const env = floorWithinCalibrateEnvelope(floor);
404
+ if (!env.ok) {
405
+ fatal(
406
+ 'floor-implausible: calibration.json `floor` does not satisfy ' +
407
+ "calibrate-render.mjs's OWN sanity envelope, so it could not have " +
408
+ 'been produced by an honest calibrate-render.mjs run (that script ' +
409
+ 'writes blocked.json β€” not calibration.json β€” when the measured ' +
410
+ 'floor fails this bound). Refusing to recompute a gate from a floor ' +
411
+ 'no real calibration could emit. Details: ' + env.reasons.join('; ')
412
+ );
413
+ }
414
+ }
415
+
416
+ // Published tolerances β€” MUST stay in sync with calibrate-render.mjs.
417
+ const round4 = (v) => Math.round(v * 10000) / 10000;
418
+ const D_SSIM = 0.005;
419
+ const D_DELTAE = 0.4;
420
+ const D_IOU = 0.03;
421
+ const GATE_EPS = 1e-4; // rounding epsilon (round4 granularity)
422
+
423
+ const expectGate = {
424
+ converged: {
425
+ ssim_nontext_min: round4(floor.ssim_nontext - D_SSIM),
426
+ deltaE_p95_max: round4(floor.deltaE_p95 + D_DELTAE),
427
+ text_iou_min: floor.text_iou === null
428
+ ? null : round4(floor.text_iou - D_IOU),
429
+ },
430
+ close: {
431
+ ssim_nontext_min: round4(floor.ssim_nontext - 2 * D_SSIM),
432
+ deltaE_p95_max: round4(floor.deltaE_p95 + 2 * D_DELTAE),
433
+ text_iou_min: floor.text_iou === null
434
+ ? null : round4(floor.text_iou - 2 * D_IOU),
435
+ },
436
+ };
437
+
438
+ function whyNot(v) { return v === undefined ? 'absent' : JSON.stringify(v); }
439
+
440
+ {
441
+ const mismatches = [];
442
+ for (const tier of ['converged', 'close']) {
443
+ const a = gate[tier], e = expectGate[tier];
444
+ for (const f of ['ssim_nontext_min', 'deltaE_p95_max', 'text_iou_min']) {
445
+ const exp = e[f];
446
+ const act = a?.[f];
447
+ if (exp === null) {
448
+ if (act !== null && act !== undefined) {
449
+ mismatches.push(
450
+ `gate.${tier}.${f}: expected null (floor.text_iou null) but ` +
451
+ `got ${whyNot(act)}`);
452
+ }
453
+ continue;
454
+ }
455
+ if (typeof act !== 'number') {
456
+ mismatches.push(
457
+ `gate.${tier}.${f}: expected ${exp} but got ${whyNot(act)}`);
458
+ } else if (Math.abs(act - exp) > GATE_EPS) {
459
+ mismatches.push(
460
+ `gate.${tier}.${f}: gate=${act} but floor implies ${exp} ` +
461
+ `(|Ξ”|=${Math.abs(act - exp).toFixed(6)} > ${GATE_EPS})`);
462
+ }
463
+ }
464
+ }
465
+ if (mismatches.length > 0) {
466
+ fatal(
467
+ 'gate-floor-mismatch: calibration.json `gate` does not match the ' +
468
+ 'gate recomputed from `floor` with the published tolerances ' +
469
+ '(ssim βˆ’0.005, deltaE +0.4, text_iou βˆ’0.03; close = 2Γ— band). A ' +
470
+ 'hand-loosened gate is rejected. Re-run calibrate-render.mjs to ' +
471
+ 'regenerate a consistent calibration. Details: ' + mismatches.join('; ')
472
+ );
473
+ }
474
+ }
475
+
476
+ // ── FIX 2: provenance β€” calibration_source binds the bundled twin IDENTITY ────
477
+ // This binds the IDENTITY of the bundled twin SOURCE FILES (excluding build
478
+ // output/dotfiles, see _provenance.mjs): recompute the source-tree hash of
479
+ // the actual bundled assets and fail closed if calibration.json claims a
480
+ // different one. It does NOT re-measure or bind the `floor` *value* β€” an
481
+ // attacker who keeps the real public twin source hashes here but writes a
482
+ // loose floor is caught by FIX A's `floor-implausible` envelope check above
483
+ // only down to calibrate-render's own sanity floor; a floor within that
484
+ // envelope yet looser than the true measured one is residual (2) (disclosed
485
+ // in the Trust-model header β€” NOT claimed closed here).
486
+
487
+ const calibSource = calib?.calibration_source;
488
+ if (!calibSource || typeof calibSource !== 'object') {
489
+ fatal(
490
+ 'calibration.json has no `calibration_source` object β€” cannot bind the ' +
491
+ 'floor to the bundled twins. Re-run the current calibrate-render.mjs ' +
492
+ '(it emits calibration_source).'
493
+ );
494
+ }
495
+ // FIX E (lighter option chosen: DROP the mandatory-presence requirement).
496
+ // `twin_hashes` is the SHA-256 of the calibration's input ref/gen PNGs.
497
+ // Those PNGs are runtime-rendered and NOT byte-stable across machines, so
498
+ // the grader cannot and does not verify `twin_hashes` β€” it is non-security
499
+ // provenance metadata only (the enforced security binding is
500
+ // `calibration_source` source-tree hashes + FIX A's sanity envelope).
501
+ // Requiring its mere presence verified nothing and falsely inflated the
502
+ // apparent binding surface (a reviewer could infer it was load-bearing), so
503
+ // the presence gate is removed. It is still passed through to the artifact
504
+ // below when present, labelled as non-security metadata.
505
+
506
+ {
507
+ const actualH5 = sourceTreeHash(BUNDLED_H5_TWIN);
508
+ const actualSwiftUI = sourceTreeHash(BUNDLED_SWIFTUI_TWIN);
509
+ const claimedH5 = calibSource.h5_twin_source_sha256;
510
+ const claimedSwiftUI = calibSource.swiftui_twin_source_sha256;
511
+ const tw = [];
512
+ if (typeof claimedH5 !== 'string' || claimedH5 !== actualH5) {
513
+ tw.push(
514
+ `h5-twin: calibration claims ${whyNot(claimedH5)} but the bundled ` +
515
+ `${BUNDLED_H5_TWIN} source tree hashes to ${actualH5}`);
516
+ }
517
+ if (typeof claimedSwiftUI !== 'string' || claimedSwiftUI !== actualSwiftUI) {
518
+ tw.push(
519
+ `swiftui-twin: calibration claims ${whyNot(claimedSwiftUI)} but the ` +
520
+ `bundled ${BUNDLED_SWIFTUI_TWIN} source tree hashes to ${actualSwiftUI}`);
521
+ }
522
+ if (tw.length > 0) {
523
+ fatal(
524
+ 'calibration-twin-mismatch: calibration.json `calibration_source` ' +
525
+ 'does not match the SHIPPED, hash-pinned bundled calibration twins. ' +
526
+ 'The measured floor must derive from a calibration run against the ' +
527
+ 'real bundled pair β€” refusing to grade against an unbound floor. ' +
528
+ tw.join('; ')
529
+ );
530
+ }
531
+ }
532
+
533
+ // ── Load judge ────────────────────────────────────────────────────────────────
534
+
535
+ const judge = readJson(judgePath, '--judge');
536
+
537
+ // ── Load masks ────────────────────────────────────────────────────────────────
538
+
539
+ let masks = [];
540
+ if (masksPath) {
541
+ const raw = readJson(masksPath, '--masks');
542
+ if (!Array.isArray(raw)) fatal('--masks must be a JSON array of {x,y,w,h,reason}.');
543
+ masks = raw;
544
+ }
545
+
546
+ // ── Load iterations ───────────────────────────────────────────────────────────
547
+
548
+ /**
549
+ * Normalize the --iterations input into [{ i, diff, built }].
550
+ * Form (a): a JSON file [{i,diff_json_path,built}, ...]
551
+ * Form (b): a directory of pixel-diff JSON files (built defaults true).
552
+ */
553
+ function loadIterations(p) {
554
+ const st = statSync(p);
555
+ const out = [];
556
+ if (st.isDirectory()) {
557
+ const files = readdirSync(p)
558
+ .filter(f => f.endsWith('.json'))
559
+ .sort();
560
+ let idx = 1;
561
+ for (const f of files) {
562
+ const full = join(p, f);
563
+ const diff = readJson(full, `iteration diff (${f})`);
564
+ const m = f.match(/iter(?:ation)?[-_]?(\d+)/i);
565
+ out.push({
566
+ i: m ? parseInt(m[1], 10) : idx,
567
+ diff,
568
+ diff_json_path: full,
569
+ built: true, // directory mode cannot record non-building iters
570
+ });
571
+ idx++;
572
+ }
573
+ if (out.length === 0) {
574
+ fatal(`--iterations directory has no *.json files: ${p}`);
575
+ }
576
+ return out;
577
+ }
578
+
579
+ // File mode
580
+ const spec = readJson(p, '--iterations');
581
+ if (!Array.isArray(spec)) {
582
+ fatal('--iterations file must be an array of {i,diff_json_path,built}.');
583
+ }
584
+ for (const entry of spec) {
585
+ if (typeof entry !== 'object' || entry === null) {
586
+ fatal('--iterations entries must be objects.');
587
+ }
588
+ if (typeof entry.i !== 'number') {
589
+ fatal('--iterations entry missing numeric `i`.');
590
+ }
591
+ if (typeof entry.built !== 'boolean') {
592
+ fatal(`--iterations entry i=${entry.i} missing boolean \`built\`.`);
593
+ }
594
+ let diff = null;
595
+ if (entry.diff_json_path) {
596
+ const dp = resolve(dirname(p), entry.diff_json_path);
597
+ const dp2 = existsSync(dp) ? dp : resolve(entry.diff_json_path);
598
+ if (!existsSync(dp2)) {
599
+ fatal(`iteration i=${entry.i} diff_json_path not found: ${entry.diff_json_path}`);
600
+ }
601
+ diff = readJson(dp2, `iteration i=${entry.i} diff`);
602
+ } else if (entry.diff && typeof entry.diff === 'object') {
603
+ diff = entry.diff; // inline diff allowed
604
+ } else {
605
+ fatal(`iteration i=${entry.i} has neither diff_json_path nor inline diff.`);
606
+ }
607
+ out.push({ i: entry.i, diff, diff_json_path: entry.diff_json_path ?? null, built: entry.built });
608
+ }
609
+ if (out.length === 0) fatal('--iterations is empty.');
610
+ return out;
611
+ }
612
+
613
+ const iterations = loadIterations(iterationsPath);
614
+
615
+ // ── Component area (mask-fraction denominator) ────────────────────────────────
616
+
617
+ /**
618
+ * Resolve the component pixel area. Precedence:
619
+ * 1. --component-area "WxH" or px integer
620
+ * 2. derive from a diff's global frame if it exposes width*height
621
+ * Returns { area:number|null, source:string }.
622
+ */
623
+ function resolveComponentArea() {
624
+ if (areaArg) {
625
+ const wh = areaArg.match(/^(\d+)\s*[xX*]\s*(\d+)$/);
626
+ if (wh) {
627
+ return { area: parseInt(wh[1], 10) * parseInt(wh[2], 10), source: `flag ${areaArg}` };
628
+ }
629
+ const px = areaArg.match(/^\d+$/);
630
+ if (px) return { area: parseInt(areaArg, 10), source: `flag ${areaArg}px` };
631
+ fatal(`--component-area must be "WxH" or a pixel integer (got "${areaArg}").`);
632
+ }
633
+ // Try to derive from a diff frame if any iteration exposes it.
634
+ for (const it of iterations) {
635
+ const d = it.diff;
636
+ if (d && typeof d.frame_w === 'number' && typeof d.frame_h === 'number') {
637
+ return { area: d.frame_w * d.frame_h, source: `diff frame i=${it.i}` };
638
+ }
639
+ }
640
+ return { area: null, source: 'unresolved' };
641
+ }
642
+
643
+ const { area: componentArea, source: areaSource } = resolveComponentArea();
644
+
645
+ // ── GUARD 1: mask budget ──────────────────────────────────────────────────────
646
+
647
+ const guardReasons = [];
648
+ let maskFraction = null;
649
+ let maskBudgetViolated = false;
650
+
651
+ for (const m of masks) {
652
+ if (typeof m.x !== 'number' || typeof m.y !== 'number'
653
+ || typeof m.w !== 'number' || typeof m.h !== 'number') {
654
+ guardReasons.push('mask-malformed: a mask is missing numeric x/y/w/h');
655
+ maskBudgetViolated = true;
656
+ }
657
+ if (typeof m.reason !== 'string' || m.reason.trim() === '') {
658
+ guardReasons.push('mask-no-reason: every mask must declare a non-empty reason');
659
+ maskBudgetViolated = true;
660
+ }
661
+ }
662
+
663
+ if (masks.length > 0) {
664
+ if (componentArea === null || componentArea <= 0) {
665
+ // Cannot verify the budget β€” refuse to silently pass. This is fatal:
666
+ // an unverifiable mask budget is an input error the caller must fix.
667
+ fatal(
668
+ 'masks supplied but component area is unknown (pass --component-area ' +
669
+ '"WxH"). Cannot verify the 10% mask budget β€” refusing to proceed ' +
670
+ 'rather than silently pass.'
671
+ );
672
+ }
673
+ const maskedPixels = masks.reduce(
674
+ (s, m) => s + Math.max(0, m.w) * Math.max(0, m.h), 0);
675
+ maskFraction = maskedPixels / componentArea;
676
+ maskFraction = Math.round(maskFraction * 10000) / 10000;
677
+ if (maskFraction > 0.10) {
678
+ maskBudgetViolated = true;
679
+ guardReasons.push(
680
+ `mask-budget: masked fraction ${maskFraction} > 0.10 ` +
681
+ `(${maskedPixels}px / ${componentArea}px area via ${areaSource})`);
682
+ }
683
+ } else {
684
+ maskFraction = 0;
685
+ }
686
+
687
+ // ── GUARD 2: gate eval in code, per iteration ─────────────────────────────────
688
+
689
+ /**
690
+ * Evaluate the structured gate against one pixel-diff output.
691
+ * Rules (visual-diff-loop-protocol.md, made executable here):
692
+ * - non-text regions: every region's ssim >= g.ssim_nontext_min AND
693
+ * deltaE_p95 <= g.deltaE_p95_max; if no non-text regions, fall back to
694
+ * diff.global_ssim against ssim_nontext_min.
695
+ * - text regions: if g.text_iou_min is null the IoU sub-gate is SKIPPED
696
+ * (calibration had no bbox map). Otherwise EVERY text region must have a
697
+ * numeric iou >= g.text_iou_min. An iou of `null` is a FAIL, never a pass.
698
+ * - pHash is necessary-not-sufficient: phash_fast_candidate is NEVER a
699
+ * short-circuit to pass. The region gate above is the sole authority.
700
+ * Returns { passed:boolean, detail:string }.
701
+ */
702
+ function evalGate(diff, g) {
703
+ const reasons = [];
704
+ if (!diff || typeof diff !== 'object') {
705
+ return { passed: false, detail: 'diff missing/not an object' };
706
+ }
707
+ const regions = diff.regions ?? {};
708
+ const nontext = Array.isArray(regions.nontext) ? regions.nontext : [];
709
+ const text = Array.isArray(regions.text) ? regions.text : [];
710
+
711
+ // Non-text sub-gate.
712
+ if (nontext.length > 0) {
713
+ for (const r of nontext) {
714
+ if (typeof r.ssim !== 'number' || r.ssim < g.ssim_nontext_min) {
715
+ reasons.push(
716
+ `nontext mark ${r.mark ?? '?'} ssim ${r.ssim ?? 'n/a'} < ` +
717
+ `${g.ssim_nontext_min}`);
718
+ }
719
+ if (typeof r.deltaE_p95 === 'number' && r.deltaE_p95 > g.deltaE_p95_max) {
720
+ reasons.push(
721
+ `nontext mark ${r.mark ?? '?'} deltaE_p95 ${r.deltaE_p95} > ` +
722
+ `${g.deltaE_p95_max}`);
723
+ }
724
+ }
725
+ } else {
726
+ const gs = diff.global_ssim;
727
+ if (typeof gs !== 'number' || gs < g.ssim_nontext_min) {
728
+ reasons.push(
729
+ `no nontext regions; global_ssim ${gs ?? 'n/a'} < ` +
730
+ `${g.ssim_nontext_min}`);
731
+ }
732
+ }
733
+
734
+ // Text sub-gate β€” only when calibration provided a text_iou floor.
735
+ if (g.text_iou_min !== null && g.text_iou_min !== undefined) {
736
+ if (text.length === 0) {
737
+ // No text regions measured but gate expects one β€” cannot confirm pass.
738
+ reasons.push(
739
+ `text_iou gate active (min ${g.text_iou_min}) but diff has no text ` +
740
+ `regions to verify`);
741
+ }
742
+ for (const r of text) {
743
+ if (r.iou === null || r.iou === undefined) {
744
+ reasons.push(
745
+ `text mark ${r.mark ?? '?'} iou is null β€” counts as FAIL ` +
746
+ `(no generated bbox supplied to pixel-diff)`);
747
+ } else if (typeof r.iou !== 'number' || r.iou < g.text_iou_min) {
748
+ reasons.push(
749
+ `text mark ${r.mark ?? '?'} iou ${r.iou} < ${g.text_iou_min}`);
750
+ }
751
+ }
752
+ }
753
+
754
+ return {
755
+ passed: reasons.length === 0,
756
+ detail: reasons.length === 0 ? 'all sub-gates passed' : reasons.join('; '),
757
+ };
758
+ }
759
+
760
+ const history = [];
761
+ for (const it of iterations) {
762
+ const conv = evalGate(it.diff, gateConverged);
763
+ const cls = evalGate(it.diff, gateClose);
764
+ history.push({
765
+ i: it.i,
766
+ built: it.built,
767
+ gate_passed: it.built && conv.passed, // gate only counts if built
768
+ close_band_passed: it.built && cls.passed,
769
+ phash_fast_candidate: it.diff?.phash_fast_candidate === true,
770
+ gate_detail: conv.detail,
771
+ diff_json_path: it.diff_json_path,
772
+ diff_summary: {
773
+ global_ssim: it.diff?.global_ssim ?? null,
774
+ phash_hamming: it.diff?.phash_hamming ?? null,
775
+ diff_pixel_fraction: it.diff?.diff_pixel_fraction ?? null,
776
+ },
777
+ });
778
+ }
779
+
780
+ // ── GUARD 3: monotone-or-fail best-of-N (this script's choice) ────────────────
781
+ // Only iterations that are BOTH built==true AND gate_passed==true are eligible.
782
+ // Caller-supplied best_iteration is ignored entirely.
783
+
784
+ const eligible = history.filter(h => h.built === true && h.gate_passed === true);
785
+ let bestIteration = null;
786
+ if (eligible.length > 0) {
787
+ // Pick the eligible iteration with the highest global_ssim, then lowest
788
+ // diff_pixel_fraction, then highest index (latest correction wins ties).
789
+ eligible.sort((a, b) => {
790
+ const sa = a.diff_summary.global_ssim ?? -Infinity;
791
+ const sb = b.diff_summary.global_ssim ?? -Infinity;
792
+ if (sb !== sa) return sb - sa;
793
+ const fa = a.diff_summary.diff_pixel_fraction ?? Infinity;
794
+ const fb = b.diff_summary.diff_pixel_fraction ?? Infinity;
795
+ if (fa !== fb) return fa - fb;
796
+ return b.i - a.i;
797
+ });
798
+ bestIteration = eligible[0].i;
799
+ }
800
+
801
+ // close-band best (used only if no converged-eligible iteration exists)
802
+ const closeEligible = history.filter(
803
+ h => h.built === true && h.close_band_passed === true);
804
+ let bestCloseIteration = null;
805
+ if (closeEligible.length > 0) {
806
+ closeEligible.sort((a, b) =>
807
+ (b.diff_summary.global_ssim ?? -Infinity) -
808
+ (a.diff_summary.global_ssim ?? -Infinity) || b.i - a.i);
809
+ bestCloseIteration = closeEligible[0].i;
810
+ }
811
+
812
+ // A built result candidate is either a converged-gate-passing iteration OR a
813
+ // built close-band-passing iteration. `needs-human` for "no passing iteration"
814
+ // only when NEITHER exists β€” a close-band iteration is still a legitimate
815
+ // (honest-accept) candidate and must not be force-failed before the close tier
816
+ // is even evaluated.
817
+ const hasBuiltPassing = bestIteration !== null;
818
+ const hasAnyBuiltCandidate = bestIteration !== null || bestCloseIteration !== null;
819
+
820
+ // ── GUARD 4: negative control voids judge β€” BOUND to the bundled twin ─────────
821
+ // FIX 3: the negative control must be a STRUCTURED artifact bound to the
822
+ // shipped, hash-pinned `assets/calibration/swiftui-twin-divergent`. The old
823
+ // bare `negative_control:"passed"` string is no longer accepted (it asserts
824
+ // a judge run that nothing verified) β€” it is treated as UNBOUND β‡’ any YES
825
+ // is VOID. The judge must demonstrate it rejected the KNOWN-divergent pair
826
+ // (whose stimulus hash this script recomputes from the shipped asset) under
827
+ // the adversarial forced-difference-3 framing with structured differences.
828
+ //
829
+ // judge.negative_control = {
830
+ // stimulus_source_hash: "<sha256 source-tree hash of the bundled
831
+ // swiftui-twin-divergent>",
832
+ // divergent_pair: "h5-twin vs swiftui-twin-divergent",
833
+ // rejected: true,
834
+ // differences: [ { desc, severity }, ... ] // non-empty, structured
835
+ // }
836
+
837
+ const expectedDivergentHash = sourceTreeHash(BUNDLED_DIVERGENT_TWIN);
838
+ const nc = judge?.negative_control;
839
+ const ncReasons = [];
840
+
841
+ if (typeof nc === 'string') {
842
+ ncReasons.push(
843
+ `negative-control-unbound: judge.negative_control is the legacy bare ` +
844
+ `string "${nc}" β€” an unbound assertion of a judge run nothing verified. ` +
845
+ `A STRUCTURED, hash-bound negative_control object is required; any YES ` +
846
+ `is VOID`);
847
+ } else if (!nc || typeof nc !== 'object') {
848
+ ncReasons.push(
849
+ `negative-control-unbound: judge.negative_control is ` +
850
+ `${nc === undefined ? 'absent' : 'not an object'} β€” a STRUCTURED, ` +
851
+ `hash-bound negative_control object is required; any YES is VOID`);
852
+ } else {
853
+ if (typeof nc.stimulus_source_hash !== 'string'
854
+ || nc.stimulus_source_hash !== expectedDivergentHash) {
855
+ ncReasons.push(
856
+ `negative-control-unbound: negative_control.stimulus_source_hash=` +
857
+ `${whyNot(nc.stimulus_source_hash)} != the bundled ` +
858
+ `swiftui-twin-divergent source-tree hash ${expectedDivergentHash} ` +
859
+ `(${BUNDLED_DIVERGENT_TWIN}) β€” the judge was not shown the shipped, ` +
860
+ `hash-pinned divergent stimulus; any YES is VOID`);
861
+ }
862
+ if (nc.rejected !== true) {
863
+ ncReasons.push(
864
+ `negative-control-unbound: negative_control.rejected=` +
865
+ `${whyNot(nc.rejected)} (must be boolean true β€” the judge MUST have ` +
866
+ `rejected the known-divergent pair); any YES is VOID`);
867
+ }
868
+ if (!Array.isArray(nc.differences) || nc.differences.length === 0
869
+ || !nc.differences.every(d =>
870
+ d && typeof d === 'object'
871
+ && typeof d.desc === 'string' && d.desc.trim() !== ''
872
+ && typeof d.severity === 'string' && d.severity.trim() !== '')) {
873
+ ncReasons.push(
874
+ `negative-control-unbound: negative_control.differences must be a ` +
875
+ `non-empty array of structured {desc,severity} entries (proof the ` +
876
+ `judge enumerated real divergences, not a bare pass); any YES is VOID`);
877
+ }
878
+ }
879
+
880
+ if (judge?.framing !== 'forced-difference-3') {
881
+ ncReasons.push(
882
+ `judge-framing: judge.framing="${judge?.framing ?? 'absent'}" != ` +
883
+ `"forced-difference-3" β€” adversarial framing not confirmed; YES is VOID`);
884
+ }
885
+
886
+ const negControlPassed = ncReasons.length === 0;
887
+ const negControlResult = negControlPassed ? 'passed' : 'failed';
888
+ if (!negControlPassed) {
889
+ for (const r of ncReasons) guardReasons.push(r);
890
+ }
891
+
892
+ const judgeVerdictRaw = String(judge?.verdict ?? '').trim();
893
+ const judgeYes = negControlPassed && judgeVerdictRaw.toUpperCase() === 'YES';
894
+ const judgeEquiv = negControlPassed &&
895
+ judgeVerdictRaw.toLowerCase() === 'visually-equivalent-residual-subperceptual';
896
+
897
+ // ── GUARD 6 + tier decision ───────────────────────────────────────────────────
898
+
899
+ const blockedPresent = blockedPath !== null && existsSync(blockedPath);
900
+
901
+ let tier;
902
+ let tierReason;
903
+
904
+ if (blockedPresent) {
905
+ tier = 'blocked';
906
+ tierReason = `blocked.json present (${blockedPath}) β€” never converged`;
907
+ } else if (maskBudgetViolated) {
908
+ tier = 'needs-human';
909
+ tierReason = `mask-guard violated: ${guardReasons.join(' | ')}`;
910
+ } else if (!hasAnyBuiltCandidate) {
911
+ tier = 'needs-human';
912
+ tierReason =
913
+ 'no iteration is BOTH built and gate/close-band passing ' +
914
+ '(monotone-or-fail): ' +
915
+ history.map(h =>
916
+ `i${h.i}[built=${h.built},gate=${h.gate_passed},` +
917
+ `close=${h.close_band_passed}]`).join(' ');
918
+ } else if (!negControlPassed) {
919
+ tier = 'needs-human';
920
+ tierReason =
921
+ 'negative control not bound / adversarial framing not satisfied β€” ' +
922
+ `judge verdict VOID: ${ncReasons.join(' | ')}`;
923
+ } else if (hasBuiltPassing && judgeYes) {
924
+ tier = 'converged';
925
+ tierReason =
926
+ `gate_passed(best i=${bestIteration}) AND judge YES with valid ` +
927
+ `negative control`;
928
+ } else if (bestCloseIteration !== null && judgeEquiv) {
929
+ tier = 'close';
930
+ // The close-band iteration is the result candidate for this tier.
931
+ bestIteration = bestCloseIteration;
932
+ tierReason =
933
+ `within calibration close band (best i=${bestCloseIteration}) AND judge ` +
934
+ `visually-equivalent-residual-subperceptual (honest accept)`;
935
+ } else {
936
+ tier = 'needs-human';
937
+ tierReason = hasBuiltPassing
938
+ ? `gate-passing iteration exists (i=${bestIteration}) but judge verdict ` +
939
+ `"${judgeVerdictRaw || 'absent'}" is not YES`
940
+ : `only a close-band iteration exists (i=${bestCloseIteration}) but ` +
941
+ `judge verdict "${judgeVerdictRaw || 'absent'}" is not ` +
942
+ `visually-equivalent-residual-subperceptual`;
943
+ }
944
+
945
+ // ── Residual (from the chosen best iteration's diff) ──────────────────────────
946
+
947
+ function residualFor(iterIndex) {
948
+ if (iterIndex === null) return null;
949
+ const it = iterations.find(x => x.i === iterIndex);
950
+ if (!it || !it.diff) return null;
951
+ const d = it.diff;
952
+ const nontext = d.regions?.nontext ?? [];
953
+ const text = d.regions?.text ?? [];
954
+ const minSsim = nontext.length
955
+ ? Math.min(...nontext.map(r => (typeof r.ssim === 'number' ? r.ssim : 1)))
956
+ : (typeof d.global_ssim === 'number' ? d.global_ssim : null);
957
+ const maxDeltaE = nontext.length
958
+ ? Math.max(...nontext.map(r => (typeof r.deltaE_p95 === 'number' ? r.deltaE_p95 : 0)))
959
+ : null;
960
+ const ious = text
961
+ .map(r => r.iou)
962
+ .filter(v => typeof v === 'number');
963
+ const minIou = ious.length ? Math.min(...ious) : null;
964
+ return {
965
+ ssim_nontext: minSsim,
966
+ deltaE_p95: maxDeltaE,
967
+ text_iou: minIou,
968
+ };
969
+ }
970
+
971
+ // ── Assemble artifact (schema: h5-to-swiftui/convergence@1) ───────────────────
972
+
973
+ const artifact = {
974
+ schema: 'h5-to-swiftui/convergence@1',
975
+ component: componentName,
976
+ // pinned-version header pass-through from calibration (do not re-derive)
977
+ pinned: calib.pinned ?? null,
978
+ calibration_floor: calib.floor
979
+ ? {
980
+ ssim_nontext: calib.floor.ssim_nontext ?? null,
981
+ deltaE_p95: calib.floor.deltaE_p95 ?? null,
982
+ text_iou: calib.floor.text_iou ?? null,
983
+ }
984
+ : null,
985
+ gate: { converged: gateConverged, close: gateClose },
986
+ iterations: history.map(h => ({
987
+ i: h.i,
988
+ diff: h.diff_summary,
989
+ built: h.built,
990
+ gate_passed: h.gate_passed,
991
+ close_band_passed: h.close_band_passed,
992
+ phash_fast_candidate: h.phash_fast_candidate,
993
+ gate_detail: h.gate_detail,
994
+ ...(h.diff_json_path ? { diff_json_path: h.diff_json_path } : {}),
995
+ })),
996
+ masks: masks.map(m => ({
997
+ x: m.x, y: m.y, w: m.w, h: m.h, reason: m.reason,
998
+ })),
999
+ mask_fraction: maskFraction,
1000
+ mask_budget: 0.10,
1001
+ component_area_px: componentArea,
1002
+ component_area_source: areaSource,
1003
+ judge: {
1004
+ negative_control: negControlResult,
1005
+ negative_control_binding: {
1006
+ expected_divergent_source_sha256: expectedDivergentHash,
1007
+ claimed_stimulus_source_hash:
1008
+ (nc && typeof nc === 'object') ? (nc.stimulus_source_hash ?? null)
1009
+ : null,
1010
+ rejected: (nc && typeof nc === 'object') ? (nc.rejected ?? null) : null,
1011
+ bound: negControlPassed,
1012
+ reasons: ncReasons,
1013
+ },
1014
+ framing: judge?.framing ?? null,
1015
+ differences: Array.isArray(judge?.differences) ? judge.differences : [],
1016
+ verdict: judgeVerdictRaw || null,
1017
+ verdict_honored: negControlPassed,
1018
+ },
1019
+ // Provenance binding asserted by this run. Precisely: the GATE was
1020
+ // recomputed from `floor`; the bundled twin SOURCE IDENTITY was verified;
1021
+ // the `floor` VALUE was asserted within calibrate-render's own sanity
1022
+ // envelope. The floor value itself is NOT re-measured by the grader β€” see
1023
+ // `residual_disclosure` below for the two named irreducible residuals.
1024
+ calibration_provenance: {
1025
+ schema_ok: true,
1026
+ gate_recomputed_from_floor: true,
1027
+ floor_within_calibrate_sanity_envelope: true, // FIX A: floor-implausible
1028
+ calibration_source: {
1029
+ h5_twin_source_sha256: calibSource.h5_twin_source_sha256 ?? null,
1030
+ swiftui_twin_source_sha256:
1031
+ calibSource.swiftui_twin_source_sha256 ?? null,
1032
+ // binds the twin SOURCE-FILE identity (excl. build output/dotfiles),
1033
+ // NOT the measured floor value
1034
+ twin_source_identity_verified_against_bundled: true,
1035
+ },
1036
+ // FIX E: twin_hashes is the SHA-256 of runtime-rendered ref/gen PNGs
1037
+ // which are NOT byte-stable across machines; it is non-security
1038
+ // provenance metadata only (NOT verified by the grader; the security
1039
+ // binding is calibration_source + the sanity envelope).
1040
+ twin_hashes_note:
1041
+ 'non-security provenance metadata (input PNG hashes, not byte-stable ' +
1042
+ 'across machines, not verified by the grader)',
1043
+ twin_hashes: (calib && typeof calib.twin_hashes === 'object')
1044
+ ? calib.twin_hashes : null,
1045
+ // FIX C: BOTH named irreducible residuals, disclosed in the artifact.
1046
+ residual_disclosure: {
1047
+ residual_1_renders_not_re_executed:
1048
+ 'the grader cannot re-execute the simulator renders; it trusts the ' +
1049
+ 'per-iteration pixel-diff JSONs came from real pixel-diff.mjs on ' +
1050
+ 'real sim-screenshot.sh renders (bounded by that script no-fake spine)',
1051
+ residual_2_floor_not_re_measured:
1052
+ 'the grader cannot re-measure the calibration floor; it asserts the ' +
1053
+ "supplied floor is within calibrate-render.mjs's own sanity envelope " +
1054
+ 'and recomputes the gate from it, but a floor within that envelope ' +
1055
+ 'yet looser than the true measured floor is trusted (mitigated by ' +
1056
+ 'the orchestrator obligation to run the real calibrate-render.mjs ' +
1057
+ 'and this calibration_provenance record)',
1058
+ },
1059
+ },
1060
+ guard_violations: guardReasons,
1061
+ best_iteration: bestIteration,
1062
+ tier,
1063
+ tier_reason: tierReason,
1064
+ residual: residualFor(bestIteration),
1065
+ evaluated_at: new Date().toISOString(),
1066
+ };
1067
+
1068
+ const json = JSON.stringify(artifact, null, 2) + '\n';
1069
+
1070
+ if (outArg) {
1071
+ const outPath = resolve(outArg);
1072
+ mkdirSync(dirname(outPath), { recursive: true });
1073
+ writeFileSync(outPath, json, 'utf8');
1074
+ console.error(`Wrote convergence artifact: ${outPath}`);
1075
+ }
1076
+
1077
+ process.stdout.write(json);
1078
+
1079
+ // ── Exit code reflects the verdict (a pipeline cannot ignore a failure) ───────
1080
+
1081
+ console.error(
1082
+ `tier=${tier} best_iteration=${bestIteration ?? 'none'} ` +
1083
+ `mask_fraction=${maskFraction} negative_control=${negControlResult}`);
1084
+ console.error(`reason: ${tierReason}`);
1085
+
1086
+ if (tier === 'converged' || tier === 'close') {
1087
+ process.exit(0);
1088
+ }
1089
+ if (tier === 'blocked') {
1090
+ process.exit(4);
1091
+ }
1092
+ // needs-human or any guard violation
1093
+ process.exit(3);