agent-learning-kit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (642) hide show
  1. agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
  2. agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
  3. agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
  4. agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
  5. agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
  6. agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
  7. fi/__init__.py +5 -0
  8. fi/alk/__init__.py +57 -0
  9. fi/alk/_facade.py +31 -0
  10. fi/alk/_module_alias.py +68 -0
  11. fi/alk/_paths.py +14 -0
  12. fi/alk/_schema.py +522 -0
  13. fi/alk/actions.py +727 -0
  14. fi/alk/bench/__init__.py +517 -0
  15. fi/alk/bench/_codeexec.py +213 -0
  16. fi/alk/bench/_coding.py +215 -0
  17. fi/alk/bench/_docker.py +237 -0
  18. fi/alk/bench/_grader.py +286 -0
  19. fi/alk/bench/_pull.py +212 -0
  20. fi/alk/bench/_voice.py +147 -0
  21. fi/alk/capabilities.py +627 -0
  22. fi/alk/cli.py +6396 -0
  23. fi/alk/config.py +130 -0
  24. fi/alk/cua_loop.py +562 -0
  25. fi/alk/evals.py +2351 -0
  26. fi/alk/extensions.py +163 -0
  27. fi/alk/harness/ARCHITECTURE.md +231 -0
  28. fi/alk/harness/DESIGN.md +246 -0
  29. fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
  30. fi/alk/harness/HOW-IT-WORKS.md +297 -0
  31. fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
  32. fi/alk/harness/README.md +417 -0
  33. fi/alk/harness/__init__.py +77 -0
  34. fi/alk/harness/__main__.py +3 -0
  35. fi/alk/harness/amend.py +312 -0
  36. fi/alk/harness/artifacts.py +319 -0
  37. fi/alk/harness/authoring_entrypoint.py +189 -0
  38. fi/alk/harness/authoring_runtime_validation.py +267 -0
  39. fi/alk/harness/backends/README.md +43 -0
  40. fi/alk/harness/backends/__init__.py +122 -0
  41. fi/alk/harness/backends/base.py +241 -0
  42. fi/alk/harness/backends/claude.py +211 -0
  43. fi/alk/harness/backends/files.py +182 -0
  44. fi/alk/harness/backends/vertex_gemini.py +457 -0
  45. fi/alk/harness/background_noise.py +95 -0
  46. fi/alk/harness/build.py +385 -0
  47. fi/alk/harness/bundle.py +593 -0
  48. fi/alk/harness/bundle_author_v2.py +1831 -0
  49. fi/alk/harness/bundle_v2.py +719 -0
  50. fi/alk/harness/call_runner.py +1440 -0
  51. fi/alk/harness/callback_http_adapter.py +111 -0
  52. fi/alk/harness/catalogue.py +287 -0
  53. fi/alk/harness/chat.py +428 -0
  54. fi/alk/harness/chat_call_runner.py +506 -0
  55. fi/alk/harness/checks.py +136 -0
  56. fi/alk/harness/cli.py +1354 -0
  57. fi/alk/harness/config.py +338 -0
  58. fi/alk/harness/contract.py +718 -0
  59. fi/alk/harness/credentials.py +674 -0
  60. fi/alk/harness/data/persona_vocabulary.json +111 -0
  61. fi/alk/harness/environment.py +99 -0
  62. fi/alk/harness/environment_plan.py +168 -0
  63. fi/alk/harness/events.py +125 -0
  64. fi/alk/harness/executor.py +304 -0
  65. fi/alk/harness/folder.py +234 -0
  66. fi/alk/harness/generated_runtime.py +815 -0
  67. fi/alk/harness/github.py +72 -0
  68. fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
  69. fi/alk/harness/hosted_entrypoint.py +2402 -0
  70. fi/alk/harness/hosted_scheduler.py +2218 -0
  71. fi/alk/harness/job.py +426 -0
  72. fi/alk/harness/judge.py +184 -0
  73. fi/alk/harness/livekit_source.py +50 -0
  74. fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
  75. fi/alk/harness/observability.py +208 -0
  76. fi/alk/harness/outbound.py +3252 -0
  77. fi/alk/harness/packaging.py +515 -0
  78. fi/alk/harness/persona_guides.py +157 -0
  79. fi/alk/harness/platform.py +692 -0
  80. fi/alk/harness/process_preflight.py +764 -0
  81. fi/alk/harness/process_runtime.py +5670 -0
  82. fi/alk/harness/prove.py +425 -0
  83. fi/alk/harness/provider_import.py +703 -0
  84. fi/alk/harness/provider_lifecycle.py +392 -0
  85. fi/alk/harness/provision.py +2896 -0
  86. fi/alk/harness/reception.py +147 -0
  87. fi/alk/harness/retell_chat_call_runner.py +373 -0
  88. fi/alk/harness/run/__init__.py +296 -0
  89. fi/alk/harness/run/alk.py +184 -0
  90. fi/alk/harness/run/call.py +162 -0
  91. fi/alk/harness/run/conversation.py +264 -0
  92. fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
  93. fi/alk/harness/run/evidence.py +195 -0
  94. fi/alk/harness/run/grade.py +598 -0
  95. fi/alk/harness/run/live.py +297 -0
  96. fi/alk/harness/run/models.py +56 -0
  97. fi/alk/harness/run/platform_evals.py +227 -0
  98. fi/alk/harness/run/sdk_voice.py +130 -0
  99. fi/alk/harness/run/simulation.py +1209 -0
  100. fi/alk/harness/run/stage.py +91 -0
  101. fi/alk/harness/run/targets.py +508 -0
  102. fi/alk/harness/run/tools.py +601 -0
  103. fi/alk/harness/run/voice.py +340 -0
  104. fi/alk/harness/runtime.py +172 -0
  105. fi/alk/harness/sandbox_server.py +2011 -0
  106. fi/alk/harness/sandbox_worker.py +44 -0
  107. fi/alk/harness/scenario.py +1048 -0
  108. fi/alk/harness/scenario_source.py +879 -0
  109. fi/alk/harness/scenario_tools.py +1143 -0
  110. fi/alk/harness/scenarios.py +915 -0
  111. fi/alk/harness/secrets.py +168 -0
  112. fi/alk/harness/service_catalog.py +97 -0
  113. fi/alk/harness/session.py +391 -0
  114. fi/alk/harness/sessions.py +372 -0
  115. fi/alk/harness/simulator.py +76 -0
  116. fi/alk/harness/simulator_voice.py +928 -0
  117. fi/alk/harness/skills/build-environment/SKILL.md +538 -0
  118. fi/alk/harness/skills/harness.md +131 -0
  119. fi/alk/harness/skills/kinds/chat.md +48 -0
  120. fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
  121. fi/alk/harness/skills/kinds/voice.md +59 -0
  122. fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
  123. fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
  124. fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
  125. fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
  126. fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
  127. fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
  128. fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
  129. fi/alk/harness/source_data_invariants.py +444 -0
  130. fi/alk/harness/source_tool_evidence.py +79 -0
  131. fi/alk/harness/sources.py +253 -0
  132. fi/alk/harness/spend.py +140 -0
  133. fi/alk/harness/tool_trace_proxy.py +104 -0
  134. fi/alk/harness/tools.py +1018 -0
  135. fi/alk/harness/understand.py +169 -0
  136. fi/alk/harness/voicemail_audio.py +74 -0
  137. fi/alk/harness/world/__init__.py +33 -0
  138. fi/alk/harness/world/errors.py +68 -0
  139. fi/alk/harness/world/expectations.py +91 -0
  140. fi/alk/harness/world/handle.py +538 -0
  141. fi/alk/harness/world/kinds.py +196 -0
  142. fi/alk/harness/world/mutate.py +186 -0
  143. fi/alk/harness/world/probe.py +413 -0
  144. fi/alk/harness/world/provision.py +511 -0
  145. fi/alk/harness/world/provisioned.py +191 -0
  146. fi/alk/harness/world/runtime.py +616 -0
  147. fi/alk/harness/world/snapshot.py +288 -0
  148. fi/alk/harness/world/stores/__init__.py +305 -0
  149. fi/alk/harness/world/stores/container.py +215 -0
  150. fi/alk/harness/world/stores/inprocess.py +346 -0
  151. fi/alk/harness/world/stores/postgres.py +481 -0
  152. fi/alk/harness/world/stores/prove.py +202 -0
  153. fi/alk/harness/world/stores/sqlite.py +245 -0
  154. fi/alk/harness/world/stores/written.py +182 -0
  155. fi/alk/harness/world/tools.py +1516 -0
  156. fi/alk/harness/world/workspace.py +144 -0
  157. fi/alk/image_loop.py +453 -0
  158. fi/alk/image_perturb.py +241 -0
  159. fi/alk/improve.py +274 -0
  160. fi/alk/live/__init__.py +154 -0
  161. fi/alk/live/_attribution.py +184 -0
  162. fi/alk/live/_capture.py +264 -0
  163. fi/alk/live/_codec.py +391 -0
  164. fi/alk/live/_contract.py +134 -0
  165. fi/alk/live/_loopback.py +316 -0
  166. fi/alk/live/_perturb.py +449 -0
  167. fi/alk/live/_runner.py +386 -0
  168. fi/alk/live/_stats.py +561 -0
  169. fi/alk/live/_transcript.py +240 -0
  170. fi/alk/live/_workers/__init__.py +9 -0
  171. fi/alk/live/_workers/a2a_worker.py +316 -0
  172. fi/alk/live/_workers/langgraph_worker.py +217 -0
  173. fi/alk/live/_workers/livekit_worker.py +207 -0
  174. fi/alk/live/_workers/mcp_loopback_server.py +46 -0
  175. fi/alk/live/_workers/mcp_worker.py +158 -0
  176. fi/alk/live/_workers/pipecat_worker.py +189 -0
  177. fi/alk/live/a2a_lane.py +138 -0
  178. fi/alk/live/langgraph_lane.py +339 -0
  179. fi/alk/live/livekit_lane.py +376 -0
  180. fi/alk/live/mcp_lane.py +172 -0
  181. fi/alk/live/pipecat_lane.py +341 -0
  182. fi/alk/live/voice_redteam.py +494 -0
  183. fi/alk/loss.py +306 -0
  184. fi/alk/optimize.py +36260 -0
  185. fi/alk/practice/__init__.py +51 -0
  186. fi/alk/practice/_assess.py +103 -0
  187. fi/alk/practice/_budget.py +81 -0
  188. fi/alk/practice/_calibrate.py +69 -0
  189. fi/alk/practice/_capstone.py +86 -0
  190. fi/alk/practice/_contract.py +91 -0
  191. fi/alk/practice/_diagnose.py +79 -0
  192. fi/alk/practice/_drill.py +196 -0
  193. fi/alk/practice/_experiment.py +720 -0
  194. fi/alk/practice/_schedule.py +102 -0
  195. fi/alk/practice/_store.py +194 -0
  196. fi/alk/practice/_trainer.py +245 -0
  197. fi/alk/practice/_update.py +125 -0
  198. fi/alk/redteam.py +2621 -0
  199. fi/alk/rewardhack.py +237 -0
  200. fi/alk/simulate.py +10351 -0
  201. fi/alk/studio/__init__.py +82 -0
  202. fi/alk/studio/_bias.py +314 -0
  203. fi/alk/studio/_calibration.py +522 -0
  204. fi/alk/studio/_coverage.py +262 -0
  205. fi/alk/studio/_download.py +665 -0
  206. fi/alk/studio/_fidelity_attack.py +114 -0
  207. fi/alk/studio/_generate.py +652 -0
  208. fi/alk/studio/_library.py +370 -0
  209. fi/alk/studio/_scan.py +134 -0
  210. fi/alk/studio/_upgrade.py +42 -0
  211. fi/alk/studio/_vendor.py +172 -0
  212. fi/alk/suite.py +4200 -0
  213. fi/alk/tasks.py +828 -0
  214. fi/alk/telemetry/__init__.py +149 -0
  215. fi/alk/telemetry/_contract.py +141 -0
  216. fi/alk/telemetry/_emit.py +182 -0
  217. fi/alk/telemetry/_ledger.py +296 -0
  218. fi/alk/telemetry/_queue.py +127 -0
  219. fi/alk/telemetry/_row.py +294 -0
  220. fi/alk/telemetry/_run.py +233 -0
  221. fi/alk/telemetry/_sync.py +193 -0
  222. fi/alk/telemetry/_url.py +119 -0
  223. fi/alk/trinity.py +49397 -0
  224. fi/alk/voice_loop.py +174 -0
  225. fi/api/__init__.py +1 -0
  226. fi/api/auth.py +137 -0
  227. fi/api/types.py +29 -0
  228. fi/cli/__init__.py +9 -0
  229. fi/cli/assertions/__init__.py +25 -0
  230. fi/cli/assertions/conditions.py +76 -0
  231. fi/cli/assertions/evaluator.py +286 -0
  232. fi/cli/assertions/exit_codes.py +20 -0
  233. fi/cli/assertions/parser.py +131 -0
  234. fi/cli/assertions/reporter.py +194 -0
  235. fi/cli/commands/__init__.py +9 -0
  236. fi/cli/commands/config.py +165 -0
  237. fi/cli/commands/export.py +208 -0
  238. fi/cli/commands/init.py +112 -0
  239. fi/cli/commands/list_cmd.py +213 -0
  240. fi/cli/commands/run.py +486 -0
  241. fi/cli/commands/validate.py +173 -0
  242. fi/cli/commands/view.py +424 -0
  243. fi/cli/config/__init__.py +6 -0
  244. fi/cli/config/defaults.py +206 -0
  245. fi/cli/config/loader.py +155 -0
  246. fi/cli/config/schema.py +174 -0
  247. fi/cli/main.py +78 -0
  248. fi/cli/output/__init__.py +6 -0
  249. fi/cli/output/formatters.py +106 -0
  250. fi/cli/output/reporters.py +46 -0
  251. fi/cli/storage/__init__.py +5 -0
  252. fi/cli/storage/run_history.py +249 -0
  253. fi/cli/utils/__init__.py +5 -0
  254. fi/cli/utils/console.py +44 -0
  255. fi/evals/__init__.py +131 -0
  256. fi/evals/autoeval/__init__.py +137 -0
  257. fi/evals/autoeval/analyzer.py +211 -0
  258. fi/evals/autoeval/config.py +244 -0
  259. fi/evals/autoeval/export.py +213 -0
  260. fi/evals/autoeval/interactive.py +283 -0
  261. fi/evals/autoeval/pipeline.py +625 -0
  262. fi/evals/autoeval/prompts.py +139 -0
  263. fi/evals/autoeval/recommender.py +242 -0
  264. fi/evals/autoeval/rules.py +589 -0
  265. fi/evals/autoeval/templates.py +299 -0
  266. fi/evals/autoeval/types.py +232 -0
  267. fi/evals/core/__init__.py +16 -0
  268. fi/evals/core/cloud_registry.py +184 -0
  269. fi/evals/core/engines.py +368 -0
  270. fi/evals/core/evaluate.py +319 -0
  271. fi/evals/core/judge_prompt.py +90 -0
  272. fi/evals/core/prompt_generator.py +83 -0
  273. fi/evals/core/registry.py +57 -0
  274. fi/evals/core/result.py +55 -0
  275. fi/evals/evaluator.py +721 -0
  276. fi/evals/execution.py +168 -0
  277. fi/evals/feedback/__init__.py +32 -0
  278. fi/evals/feedback/calibrator.py +160 -0
  279. fi/evals/feedback/collector.py +214 -0
  280. fi/evals/feedback/hooks.py +81 -0
  281. fi/evals/feedback/retriever.py +128 -0
  282. fi/evals/feedback/store.py +272 -0
  283. fi/evals/feedback/types.py +99 -0
  284. fi/evals/framework/README.md +79 -0
  285. fi/evals/framework/__init__.py +267 -0
  286. fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
  287. fi/evals/framework/backends/__init__.py +99 -0
  288. fi/evals/framework/backends/_container.py +141 -0
  289. fi/evals/framework/backends/_utils.py +145 -0
  290. fi/evals/framework/backends/base.py +223 -0
  291. fi/evals/framework/backends/celery_backend.py +417 -0
  292. fi/evals/framework/backends/celery_worker.py +78 -0
  293. fi/evals/framework/backends/kubernetes_backend.py +665 -0
  294. fi/evals/framework/backends/ray_backend.py +521 -0
  295. fi/evals/framework/backends/temporal.py +350 -0
  296. fi/evals/framework/backends/temporal_worker.py +126 -0
  297. fi/evals/framework/backends/thread_pool.py +286 -0
  298. fi/evals/framework/context.py +258 -0
  299. fi/evals/framework/enrichment.py +306 -0
  300. fi/evals/framework/evals/__init__.py +68 -0
  301. fi/evals/framework/evals/agentic.py +399 -0
  302. fi/evals/framework/evals/builder.py +609 -0
  303. fi/evals/framework/evals/semantic.py +142 -0
  304. fi/evals/framework/evaluator.py +647 -0
  305. fi/evals/framework/evaluators/__init__.py +22 -0
  306. fi/evals/framework/evaluators/blocking.py +347 -0
  307. fi/evals/framework/evaluators/non_blocking.py +577 -0
  308. fi/evals/framework/propagation.py +421 -0
  309. fi/evals/framework/protocols.py +385 -0
  310. fi/evals/framework/registry.py +370 -0
  311. fi/evals/framework/resilience/__init__.py +150 -0
  312. fi/evals/framework/resilience/circuit_breaker.py +309 -0
  313. fi/evals/framework/resilience/degradation.py +355 -0
  314. fi/evals/framework/resilience/health.py +505 -0
  315. fi/evals/framework/resilience/rate_limiter.py +228 -0
  316. fi/evals/framework/resilience/retry.py +274 -0
  317. fi/evals/framework/resilience/types.py +288 -0
  318. fi/evals/framework/resilience/wrapper.py +433 -0
  319. fi/evals/framework/types.py +218 -0
  320. fi/evals/guardrails/README.md +915 -0
  321. fi/evals/guardrails/__init__.py +96 -0
  322. fi/evals/guardrails/backends/__init__.py +43 -0
  323. fi/evals/guardrails/backends/azure.py +361 -0
  324. fi/evals/guardrails/backends/base.py +88 -0
  325. fi/evals/guardrails/backends/generic_llm.py +163 -0
  326. fi/evals/guardrails/backends/granite.py +216 -0
  327. fi/evals/guardrails/backends/llamaguard.py +221 -0
  328. fi/evals/guardrails/backends/local_base.py +479 -0
  329. fi/evals/guardrails/backends/openai.py +365 -0
  330. fi/evals/guardrails/backends/qwen.py +170 -0
  331. fi/evals/guardrails/backends/shieldgemma.py +154 -0
  332. fi/evals/guardrails/backends/turing.py +235 -0
  333. fi/evals/guardrails/backends/vllm_client.py +321 -0
  334. fi/evals/guardrails/backends/wildguard.py +188 -0
  335. fi/evals/guardrails/base.py +888 -0
  336. fi/evals/guardrails/config.py +221 -0
  337. fi/evals/guardrails/discovery.py +243 -0
  338. fi/evals/guardrails/gateway.py +437 -0
  339. fi/evals/guardrails/registry.py +231 -0
  340. fi/evals/guardrails/scanners/__init__.py +127 -0
  341. fi/evals/guardrails/scanners/base.py +191 -0
  342. fi/evals/guardrails/scanners/code_injection.py +243 -0
  343. fi/evals/guardrails/scanners/eval_delegate.py +574 -0
  344. fi/evals/guardrails/scanners/invisible_chars.py +351 -0
  345. fi/evals/guardrails/scanners/jailbreak.py +412 -0
  346. fi/evals/guardrails/scanners/language.py +288 -0
  347. fi/evals/guardrails/scanners/pipeline.py +260 -0
  348. fi/evals/guardrails/scanners/regex.py +311 -0
  349. fi/evals/guardrails/scanners/secrets.py +274 -0
  350. fi/evals/guardrails/scanners/topics.py +649 -0
  351. fi/evals/guardrails/scanners/urls.py +341 -0
  352. fi/evals/guardrails/types.py +96 -0
  353. fi/evals/llm/__init__.py +3 -0
  354. fi/evals/llm/base_llm_provider.py +35 -0
  355. fi/evals/llm/providers/litellm.py +70 -0
  356. fi/evals/local/__init__.py +90 -0
  357. fi/evals/local/evaluator.py +690 -0
  358. fi/evals/local/execution_mode.py +121 -0
  359. fi/evals/local/llm.py +489 -0
  360. fi/evals/local/metrics/__init__.py +19 -0
  361. fi/evals/local/registry.py +360 -0
  362. fi/evals/manager.py +1018 -0
  363. fi/evals/manager_types.py +362 -0
  364. fi/evals/metrics/__init__.py +185 -0
  365. fi/evals/metrics/agents/__init__.py +74 -0
  366. fi/evals/metrics/agents/metrics.py +693 -0
  367. fi/evals/metrics/agents/report.py +36463 -0
  368. fi/evals/metrics/agents/types.py +160 -0
  369. fi/evals/metrics/base_llm_metric.py +111 -0
  370. fi/evals/metrics/base_metric.py +138 -0
  371. fi/evals/metrics/code_security/__init__.py +305 -0
  372. fi/evals/metrics/code_security/analyzer.py +985 -0
  373. fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
  374. fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
  375. fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
  376. fi/evals/metrics/code_security/benchmarks/types.py +308 -0
  377. fi/evals/metrics/code_security/detectors/__init__.py +186 -0
  378. fi/evals/metrics/code_security/detectors/base.py +394 -0
  379. fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
  380. fi/evals/metrics/code_security/detectors/injection.py +744 -0
  381. fi/evals/metrics/code_security/detectors/secrets.py +287 -0
  382. fi/evals/metrics/code_security/detectors/serialization.py +192 -0
  383. fi/evals/metrics/code_security/joint_metrics.py +588 -0
  384. fi/evals/metrics/code_security/judges/__init__.py +83 -0
  385. fi/evals/metrics/code_security/judges/base.py +238 -0
  386. fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
  387. fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
  388. fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
  389. fi/evals/metrics/code_security/metrics.py +388 -0
  390. fi/evals/metrics/code_security/modes/__init__.py +63 -0
  391. fi/evals/metrics/code_security/modes/adversarial.py +284 -0
  392. fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
  393. fi/evals/metrics/code_security/modes/base.py +283 -0
  394. fi/evals/metrics/code_security/modes/instruct.py +253 -0
  395. fi/evals/metrics/code_security/modes/repair.py +230 -0
  396. fi/evals/metrics/code_security/reports/__init__.py +57 -0
  397. fi/evals/metrics/code_security/reports/generator.py +404 -0
  398. fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
  399. fi/evals/metrics/code_security/types.py +534 -0
  400. fi/evals/metrics/function_calling/__init__.py +34 -0
  401. fi/evals/metrics/function_calling/metrics.py +573 -0
  402. fi/evals/metrics/function_calling/types.py +87 -0
  403. fi/evals/metrics/hallucination/__init__.py +54 -0
  404. fi/evals/metrics/hallucination/detector.py +149 -0
  405. fi/evals/metrics/hallucination/metrics.py +390 -0
  406. fi/evals/metrics/hallucination/nli.py +253 -0
  407. fi/evals/metrics/hallucination/sentinel.py +106 -0
  408. fi/evals/metrics/hallucination/types.py +132 -0
  409. fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
  410. fi/evals/metrics/heuristics/json_metrics.py +87 -0
  411. fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
  412. fi/evals/metrics/heuristics/string_metrics.py +391 -0
  413. fi/evals/metrics/llm_as_judges/__init__.py +17 -0
  414. fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
  415. fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
  416. fi/evals/metrics/llm_as_judges/types.py +48 -0
  417. fi/evals/metrics/rag/__init__.py +111 -0
  418. fi/evals/metrics/rag/advanced/__init__.py +14 -0
  419. fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
  420. fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
  421. fi/evals/metrics/rag/generation/__init__.py +17 -0
  422. fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
  423. fi/evals/metrics/rag/generation/context_utilization.py +245 -0
  424. fi/evals/metrics/rag/generation/faithfulness.py +241 -0
  425. fi/evals/metrics/rag/generation/groundedness.py +131 -0
  426. fi/evals/metrics/rag/rag_score.py +277 -0
  427. fi/evals/metrics/rag/retrieval/__init__.py +20 -0
  428. fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
  429. fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
  430. fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
  431. fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
  432. fi/evals/metrics/rag/retrieval/ranking.py +261 -0
  433. fi/evals/metrics/rag/types.py +100 -0
  434. fi/evals/metrics/rag/utils/__init__.py +62 -0
  435. fi/evals/metrics/rag/utils/claims.py +189 -0
  436. fi/evals/metrics/rag/utils/entities.py +244 -0
  437. fi/evals/metrics/rag/utils/nli.py +92 -0
  438. fi/evals/metrics/rag/utils/similarity.py +345 -0
  439. fi/evals/metrics/structured/__init__.py +114 -0
  440. fi/evals/metrics/structured/field_completeness.py +313 -0
  441. fi/evals/metrics/structured/hierarchy_score.py +366 -0
  442. fi/evals/metrics/structured/json_validation.py +190 -0
  443. fi/evals/metrics/structured/schema_compliance.py +280 -0
  444. fi/evals/metrics/structured/structured_output_score.py +298 -0
  445. fi/evals/metrics/structured/types.py +108 -0
  446. fi/evals/metrics/structured/validators/__init__.py +30 -0
  447. fi/evals/metrics/structured/validators/base.py +189 -0
  448. fi/evals/metrics/structured/validators/json_validator.py +196 -0
  449. fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
  450. fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
  451. fi/evals/otel/__init__.py +266 -0
  452. fi/evals/otel/config.py +400 -0
  453. fi/evals/otel/conventions.py +463 -0
  454. fi/evals/otel/enrichment.py +371 -0
  455. fi/evals/otel/instrumentors/__init__.py +140 -0
  456. fi/evals/otel/instrumentors/anthropic.py +517 -0
  457. fi/evals/otel/instrumentors/base.py +382 -0
  458. fi/evals/otel/instrumentors/openai.py +673 -0
  459. fi/evals/otel/processors/__init__.py +36 -0
  460. fi/evals/otel/processors/base.py +473 -0
  461. fi/evals/otel/processors/cost.py +445 -0
  462. fi/evals/otel/processors/evaluation.py +559 -0
  463. fi/evals/otel/processors/llm.py +462 -0
  464. fi/evals/otel/tracer.py +506 -0
  465. fi/evals/otel/types.py +232 -0
  466. fi/evals/otel_utils.py +23 -0
  467. fi/evals/protect.py +671 -0
  468. fi/evals/protect_input_adapter.py +154 -0
  469. fi/evals/streaming/__init__.py +88 -0
  470. fi/evals/streaming/buffer.py +213 -0
  471. fi/evals/streaming/evaluator.py +551 -0
  472. fi/evals/streaming/policy.py +307 -0
  473. fi/evals/streaming/scorers.py +368 -0
  474. fi/evals/streaming/types.py +238 -0
  475. fi/evals/templates.py +472 -0
  476. fi/evals/types.py +156 -0
  477. fi/opt/__init__.py +221 -0
  478. fi/opt/_objective_scoring.py +85 -0
  479. fi/opt/base/__init__.py +11 -0
  480. fi/opt/base/base_generator.py +33 -0
  481. fi/opt/base/base_mapper.py +26 -0
  482. fi/opt/base/base_optimizer.py +45 -0
  483. fi/opt/base/evaluator.py +211 -0
  484. fi/opt/components.py +3095 -0
  485. fi/opt/datamappers/__init__.py +3 -0
  486. fi/opt/datamappers/basic_mapper.py +40 -0
  487. fi/opt/deployment.py +1021 -0
  488. fi/opt/evidence.py +4332 -0
  489. fi/opt/generators/__init__.py +3 -0
  490. fi/opt/generators/litellm.py +66 -0
  491. fi/opt/integrations/__init__.py +23 -0
  492. fi/opt/integrations/generative_suite.py +410 -0
  493. fi/opt/integrations/simulate.py +1313 -0
  494. fi/opt/mutations.py +771 -0
  495. fi/opt/observability.py +4639 -0
  496. fi/opt/optimizer_trace.py +889 -0
  497. fi/opt/optimizers/__init__.py +80 -0
  498. fi/opt/optimizers/agent.py +331 -0
  499. fi/opt/optimizers/agent_bandit.py +392 -0
  500. fi/opt/optimizers/agent_curriculum.py +635 -0
  501. fi/opt/optimizers/agent_evolution.py +894 -0
  502. fi/opt/optimizers/agent_feedback.py +1863 -0
  503. fi/opt/optimizers/agent_pareto.py +547 -0
  504. fi/opt/optimizers/agent_social_memory.py +1113 -0
  505. fi/opt/optimizers/agent_tpe.py +321 -0
  506. fi/opt/optimizers/bayesian_search.py +449 -0
  507. fi/opt/optimizers/council.py +2075 -0
  508. fi/opt/optimizers/futureagi_replay.py +799 -0
  509. fi/opt/optimizers/gepa.py +322 -0
  510. fi/opt/optimizers/metaprompt.py +243 -0
  511. fi/opt/optimizers/promptwizard.py +417 -0
  512. fi/opt/optimizers/protegi.py +329 -0
  513. fi/opt/optimizers/random_search.py +224 -0
  514. fi/opt/research.py +518 -0
  515. fi/opt/simulation.py +260 -0
  516. fi/opt/targets.py +232 -0
  517. fi/opt/types.py +66 -0
  518. fi/opt/utils/__init__.py +4 -0
  519. fi/opt/utils/early_stopping.py +266 -0
  520. fi/opt/utils/setup_logging.py +82 -0
  521. fi/simulate/__init__.py +540 -0
  522. fi/simulate/_hashing.py +35 -0
  523. fi/simulate/_logging.py +10 -0
  524. fi/simulate/adapters.py +87 -0
  525. fi/simulate/agent/__init__.py +120 -0
  526. fi/simulate/agent/browser.py +658 -0
  527. fi/simulate/agent/definition.py +587 -0
  528. fi/simulate/agent/frameworks.py +3528 -0
  529. fi/simulate/agent/generic.py +8286 -0
  530. fi/simulate/agent/import_probe.py +227 -0
  531. fi/simulate/agent/memory.py +905 -0
  532. fi/simulate/agent/mocks.py +101 -0
  533. fi/simulate/agent/multi_agent.py +361 -0
  534. fi/simulate/agent/orchestration.py +903 -0
  535. fi/simulate/agent/realtime.py +665 -0
  536. fi/simulate/agent/wrapper.py +99 -0
  537. fi/simulate/agent/wrappers/__init__.py +18 -0
  538. fi/simulate/agent/wrappers/anthropic.py +62 -0
  539. fi/simulate/agent/wrappers/gemini.py +65 -0
  540. fi/simulate/agent/wrappers/http.py +404 -0
  541. fi/simulate/agent/wrappers/langchain.py +80 -0
  542. fi/simulate/agent/wrappers/openai.py +75 -0
  543. fi/simulate/agent/wrappers/websocket.py +326 -0
  544. fi/simulate/artifacts/__init__.py +11 -0
  545. fi/simulate/artifacts/manifest.py +62 -0
  546. fi/simulate/cli.py +20560 -0
  547. fi/simulate/endpoints/__init__.py +45 -0
  548. fi/simulate/endpoints/_http_actor.py +73 -0
  549. fi/simulate/endpoints/actor_sources.py +243 -0
  550. fi/simulate/endpoints/base.py +107 -0
  551. fi/simulate/endpoints/builtins.py +10 -0
  552. fi/simulate/endpoints/callable.py +95 -0
  553. fi/simulate/endpoints/http.py +76 -0
  554. fi/simulate/endpoints/livekit.py +138 -0
  555. fi/simulate/endpoints/originators.py +132 -0
  556. fi/simulate/endpoints/profiles.py +348 -0
  557. fi/simulate/endpoints/retell.py +633 -0
  558. fi/simulate/endpoints/vapi.py +205 -0
  559. fi/simulate/endpoints/websocket.py +76 -0
  560. fi/simulate/environment.py +33026 -0
  561. fi/simulate/environments/__init__.py +11 -0
  562. fi/simulate/environments/base.py +73 -0
  563. fi/simulate/environments/chat.py +697 -0
  564. fi/simulate/environments/voice.py +212 -0
  565. fi/simulate/evaluation/__init__.py +4 -0
  566. fi/simulate/evaluation/ai_eval.py +227 -0
  567. fi/simulate/evidence/__init__.py +35 -0
  568. fi/simulate/evidence/base.py +59 -0
  569. fi/simulate/evidence/caller_observed.py +50 -0
  570. fi/simulate/evidence/livekit_instrumentation.py +51 -0
  571. fi/simulate/evidence/livekit_room.py +50 -0
  572. fi/simulate/evidence/otel.py +49 -0
  573. fi/simulate/evidence/providers/__init__.py +24 -0
  574. fi/simulate/evidence/providers/base.py +61 -0
  575. fi/simulate/evidence/providers/retell.py +376 -0
  576. fi/simulate/evidence/providers/vapi.py +426 -0
  577. fi/simulate/hosted/__init__.py +32 -0
  578. fi/simulate/hosted/child_entrypoint.py +306 -0
  579. fi/simulate/hosted/job.py +150 -0
  580. fi/simulate/hosted/targets.py +53 -0
  581. fi/simulate/instrumentation/__init__.py +5 -0
  582. fi/simulate/instrumentation/livekit/__init__.py +122 -0
  583. fi/simulate/manifest.py +1033 -0
  584. fi/simulate/matrix_cli.py +165 -0
  585. fi/simulate/realtime/__init__.py +40 -0
  586. fi/simulate/realtime/events.py +107 -0
  587. fi/simulate/realtime/media.py +61 -0
  588. fi/simulate/realtime/session.py +91 -0
  589. fi/simulate/recording/__init__.py +5 -0
  590. fi/simulate/recording/room_recorder.py +326 -0
  591. fi/simulate/registry.py +185 -0
  592. fi/simulate/results/__init__.py +9 -0
  593. fi/simulate/results/base.py +18 -0
  594. fi/simulate/results/filesystem.py +71 -0
  595. fi/simulate/results/futureagi.py +1340 -0
  596. fi/simulate/runtime/__init__.py +85 -0
  597. fi/simulate/runtime/capabilities.py +40 -0
  598. fi/simulate/runtime/events.py +63 -0
  599. fi/simulate/runtime/failures.py +25 -0
  600. fi/simulate/runtime/ids.py +34 -0
  601. fi/simulate/runtime/plan.py +70 -0
  602. fi/simulate/runtime/planner.py +102 -0
  603. fi/simulate/runtime/report.py +174 -0
  604. fi/simulate/runtime/run.py +75 -0
  605. fi/simulate/runtime/runner.py +333 -0
  606. fi/simulate/runtime/spec.py +186 -0
  607. fi/simulate/simulation/__init__.py +30 -0
  608. fi/simulate/simulation/behavior_policy.py +425 -0
  609. fi/simulate/simulation/bridge/__init__.py +9 -0
  610. fi/simulate/simulation/bridge/audio.py +29 -0
  611. fi/simulate/simulation/bridge/connector.py +46 -0
  612. fi/simulate/simulation/bridge/livekit.py +252 -0
  613. fi/simulate/simulation/bridge/retell.py +188 -0
  614. fi/simulate/simulation/bridge/vapi.py +177 -0
  615. fi/simulate/simulation/contract.py +419 -0
  616. fi/simulate/simulation/engines/__init__.py +12 -0
  617. fi/simulate/simulation/engines/base.py +21 -0
  618. fi/simulate/simulation/engines/cloud.py +517 -0
  619. fi/simulate/simulation/engines/livekit.py +4167 -0
  620. fi/simulate/simulation/engines/local_text.py +89 -0
  621. fi/simulate/simulation/fidelity.py +374 -0
  622. fi/simulate/simulation/gemini_tts_stream.py +110 -0
  623. fi/simulate/simulation/generator.py +91 -0
  624. fi/simulate/simulation/goal_machine.py +185 -0
  625. fi/simulate/simulation/livekit_models.py +467 -0
  626. fi/simulate/simulation/matrix.py +170 -0
  627. fi/simulate/simulation/models.py +279 -0
  628. fi/simulate/simulation/runner.py +153 -0
  629. fi/simulate/simulation/synthetic.py +880 -0
  630. fi/simulate/simulation/voice_prompt.py +502 -0
  631. fi/simulate/simulator/__init__.py +55 -0
  632. fi/simulate/simulator/builtins.py +53 -0
  633. fi/simulate/suite.py +1288 -0
  634. fi/simulate/utils/routes.py +164 -0
  635. fi/simulate/voice.py +225 -0
  636. fi/simulate/voice_cli.py +182 -0
  637. fi/utils/__init__.py +1 -0
  638. fi/utils/constants.py +14 -0
  639. fi/utils/errors.py +200 -0
  640. fi/utils/executor.py +26 -0
  641. fi/utils/routes.py +119 -0
  642. fi/utils/utils.py +17 -0
@@ -0,0 +1,889 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, Mapping, Optional
4
+
5
+ from .types import OptimizationResult
6
+
7
+ # Canon vocabularies live in council.py (single home); imported here for
8
+ # governance-record validation. Safe: council does not import this module.
9
+ from .optimizers.council import (
10
+ CHAMBER_TOKENS,
11
+ GUNA_AXES,
12
+ HETVABHASA_REJECTION_CLASSES,
13
+ PANCA_AVAYAVA_MEMBERS,
14
+ _validate_justification,
15
+ )
16
+
17
+ OPTIMIZER_TRAJECTORY_PROFILE_KIND = "agent-learning.optimizer-trajectory-profile.v1"
18
+
19
+ _CRITIQUE_KIND_BY_ROLE_KIND = {
20
+ "critic": "vada",
21
+ "adversary": "jalpa",
22
+ }
23
+
24
+
25
+ def optimizer_trajectory_profile(result: OptimizationResult) -> dict[str, Any]:
26
+ """Trajectory fitness profile (ACL-Findings 2026, arXiv:2604.19440):
27
+ trajectory shape, not endpoint score, as backend-routing evidence.
28
+
29
+ Computed post-hoc from ``OptimizationResult.history`` — no backend loop
30
+ changes, every backend gets it for free.
31
+ """
32
+
33
+ history = list(result.history or [])
34
+ metadata = dict(result.metadata or {})
35
+
36
+ running_best: Optional[float] = None
37
+ improvements = 0
38
+ locality_terms: list[float] = []
39
+ regression_count = 0
40
+ scores_by_candidate: Dict[str, float] = {}
41
+ previous_score: Optional[float] = None
42
+ candidate_keys: list[str] = []
43
+
44
+ for index, item in enumerate(history):
45
+ score = float(getattr(item, "average_score", 0.0) or 0.0)
46
+ item_metadata = dict(getattr(item, "metadata", {}) or {})
47
+ candidate_id = str(
48
+ getattr(item, "candidate_id", None)
49
+ or item_metadata.get("candidate_id")
50
+ or f"iteration-{index}"
51
+ )
52
+ candidate_keys.append(candidate_id)
53
+
54
+ improved = running_best is None or score > running_best
55
+ if improved and index > 0:
56
+ improvements += 1
57
+ if improved:
58
+ running_best = score
59
+ patch = item_metadata.get("patch") or item_metadata.get(
60
+ "candidate_patch"
61
+ )
62
+ paths_touched = len(patch) if isinstance(patch, Mapping) else 1
63
+ locality_terms.append(1.0 / max(1, paths_touched))
64
+
65
+ parent_ids = [
66
+ str(parent)
67
+ for parent in _as_list(
68
+ item_metadata.get("proposal_parent_ids")
69
+ or item_metadata.get("evolution_parent_ids")
70
+ )
71
+ if str(parent)
72
+ ]
73
+ parent_scores = [
74
+ scores_by_candidate[parent]
75
+ for parent in parent_ids
76
+ if parent in scores_by_candidate
77
+ ]
78
+ if parent_scores:
79
+ if score < max(parent_scores):
80
+ regression_count += 1
81
+ elif previous_score is not None and score < previous_score:
82
+ regression_count += 1
83
+
84
+ scores_by_candidate.setdefault(candidate_id, score)
85
+ previous_score = score
86
+
87
+ iteration_count = len(history)
88
+ comparable = max(1, iteration_count - 1)
89
+ return {
90
+ # Embedded payload, not a top-level artifact kind.
91
+ "kind": OPTIMIZER_TRAJECTORY_PROFILE_KIND,
92
+ "improvement_frequency": round(improvements / comparable, 4)
93
+ if iteration_count > 1
94
+ else (1.0 if iteration_count == 1 else 0.0),
95
+ "semantic_locality": round(
96
+ sum(locality_terms) / len(locality_terms), 4
97
+ )
98
+ if locality_terms
99
+ else 0.0,
100
+ "dedupe_rate": round(
101
+ 1.0 - (len(set(candidate_keys)) / iteration_count), 4
102
+ )
103
+ if iteration_count
104
+ else 0.0,
105
+ "regression_count": regression_count,
106
+ "iterations": iteration_count,
107
+ "evaluations": int(result.total_evaluations or 0),
108
+ "early_stopped": bool(result.early_stopped),
109
+ "selection": metadata.get("selection"),
110
+ "eval_budget": metadata.get("eval_budget"),
111
+ }
112
+
113
+
114
+ def build_optimizer_society_trace(
115
+ result: OptimizationResult,
116
+ *,
117
+ name: Optional[str] = None,
118
+ metadata: Optional[Mapping[str, Any]] = None,
119
+ ) -> Dict[str, Any]:
120
+ """Export a council/society optimization run as portable trace evidence."""
121
+
122
+ result_metadata = dict(result.metadata or {})
123
+ roles = _role_records(result_metadata)
124
+ proposals = [_proposal_record(item) for item in result.history]
125
+ proposals = [item for item in proposals if item]
126
+ for proposal in proposals:
127
+ justification = dict(proposal.get("metadata") or {}).get("justification")
128
+ if justification is not None:
129
+ _validate_justification(justification)
130
+ search_paths = sorted(str(path) for path in result_metadata.get("search_paths", []) if str(path))
131
+ diagnostics = [
132
+ dict(item)
133
+ for item in _as_list(result_metadata.get("diagnostics"))
134
+ if isinstance(item, Mapping)
135
+ ]
136
+ role_credit = _role_credit(proposals)
137
+ best_candidate_id = str(result_metadata.get("best_candidate_id") or getattr(result.best_candidate, "id", "") or "")
138
+ final_score = float(result.final_score)
139
+ governance = _governance_records(
140
+ result_metadata=result_metadata,
141
+ roles=roles,
142
+ proposals=proposals,
143
+ diagnostics=diagnostics,
144
+ search_paths=search_paths,
145
+ role_credit=role_credit,
146
+ best_candidate_id=best_candidate_id,
147
+ final_score=final_score,
148
+ )
149
+ signals = _signals(
150
+ roles=roles,
151
+ proposals=proposals,
152
+ diagnostics=diagnostics,
153
+ search_paths=search_paths,
154
+ role_credit=role_credit,
155
+ best_candidate_id=best_candidate_id,
156
+ governance=governance,
157
+ )
158
+ summary = _summary(
159
+ roles=roles,
160
+ proposals=proposals,
161
+ diagnostics=diagnostics,
162
+ search_paths=search_paths,
163
+ role_credit=role_credit,
164
+ best_candidate_id=best_candidate_id,
165
+ final_score=final_score,
166
+ rounds=_as_list(result_metadata.get("rounds")),
167
+ governance=governance,
168
+ )
169
+ ledger_records = [
170
+ dict(item)
171
+ for item in _as_list(result_metadata.get("ledger_rounds"))
172
+ if isinstance(item, Mapping)
173
+ ]
174
+ return {
175
+ "kind": "optimizer_society_trace",
176
+ "name": name or str(result_metadata.get("target_name") or "optimizer-society-trace"),
177
+ "optimizer": str(result_metadata.get("optimizer") or "agent-opt"),
178
+ "strategy": result_metadata.get("strategy"),
179
+ "roles": roles,
180
+ "proposals": proposals,
181
+ "rounds": [dict(item) for item in _as_list(result_metadata.get("rounds")) if isinstance(item, Mapping)],
182
+ "diagnostics": diagnostics,
183
+ "search_paths": search_paths,
184
+ "role_credit": role_credit,
185
+ "governance": governance,
186
+ "ledger": ledger_records,
187
+ "best_candidate_id": best_candidate_id or None,
188
+ "final_score": final_score,
189
+ "signals": sorted(signals),
190
+ "summary": summary,
191
+ "metadata": {
192
+ "source": "agent-opt",
193
+ **{k: v for k, v in result_metadata.items() if k not in {"rounds", "diagnostics"}},
194
+ **dict(metadata or {}),
195
+ },
196
+ }
197
+
198
+
199
+ def _role_records(metadata: Mapping[str, Any]) -> list[Dict[str, Any]]:
200
+ role_graph = [
201
+ dict(item)
202
+ for item in _as_list(metadata.get("role_graph"))
203
+ if isinstance(item, Mapping)
204
+ ]
205
+ if role_graph:
206
+ return role_graph
207
+ return [{"name": str(role)} for role in _as_list(metadata.get("roles")) if str(role)]
208
+
209
+
210
+ def _proposal_record(history: Any) -> Dict[str, Any]:
211
+ item_metadata = dict(getattr(history, "metadata", {}) or {})
212
+ proposal_metadata = dict(item_metadata.get("proposal_metadata") or {})
213
+ role = str(item_metadata.get("proposal_role") or "unknown")
214
+ patch = dict(item_metadata.get("patch") or {})
215
+ return {
216
+ "id": str(getattr(history, "candidate_id", None) or item_metadata.get("candidate_id") or ""),
217
+ "candidate_id": str(getattr(history, "candidate_id", None) or item_metadata.get("candidate_id") or ""),
218
+ "role": role,
219
+ "round": item_metadata.get("proposal_round"),
220
+ "score": float(getattr(history, "average_score", 0.0)),
221
+ "reason": str(item_metadata.get("proposal_reason") or item_metadata.get("reason") or ""),
222
+ "parent_ids": [
223
+ str(parent)
224
+ for parent in _as_list(item_metadata.get("proposal_parent_ids"))
225
+ if str(parent)
226
+ ],
227
+ "patch": patch,
228
+ "search_paths": sorted(str(path) for path in patch.keys()),
229
+ "role_kind": str(item_metadata.get("role_kind") or proposal_metadata.get("role_kind") or ""),
230
+ "role_archetype": str(item_metadata.get("role_archetype") or proposal_metadata.get("role_archetype") or ""),
231
+ "metadata": proposal_metadata,
232
+ }
233
+
234
+
235
+ def _role_credit(proposals: Iterable[Mapping[str, Any]]) -> list[Dict[str, Any]]:
236
+ credit: Dict[str, Dict[str, Any]] = {}
237
+ for proposal in proposals:
238
+ role = str(proposal.get("role") or "unknown")
239
+ key = _normalize(role) or "unknown"
240
+ score = _optional_float(proposal.get("score"))
241
+ entry = credit.setdefault(
242
+ key,
243
+ {
244
+ "role": role,
245
+ "proposal_count": 0,
246
+ "evaluated_count": 0,
247
+ "best_score": None,
248
+ "best_candidate_id": None,
249
+ "search_paths": set(),
250
+ },
251
+ )
252
+ entry["proposal_count"] += 1
253
+ entry["search_paths"].update(str(path) for path in _as_list(proposal.get("search_paths")) if str(path))
254
+ if score is None:
255
+ continue
256
+ entry["evaluated_count"] += 1
257
+ if entry["best_score"] is None or score > float(entry["best_score"]):
258
+ entry["best_score"] = score
259
+ entry["best_candidate_id"] = proposal.get("candidate_id")
260
+ return [
261
+ {
262
+ **entry,
263
+ "search_paths": sorted(entry["search_paths"]),
264
+ }
265
+ for entry in sorted(credit.values(), key=lambda item: str(item["role"]))
266
+ ]
267
+
268
+
269
+ def _signals(
270
+ *,
271
+ roles: list[Mapping[str, Any]],
272
+ proposals: list[Mapping[str, Any]],
273
+ diagnostics: list[Mapping[str, Any]],
274
+ search_paths: list[str],
275
+ role_credit: list[Mapping[str, Any]],
276
+ best_candidate_id: str,
277
+ governance: Mapping[str, Any],
278
+ ) -> set[str]:
279
+ signals = {"optimizer", "society_trace"}
280
+ if roles:
281
+ signals.add("role")
282
+ if any(role.get("proposal_kind") for role in roles) or any(proposal.get("role_kind") for proposal in proposals):
283
+ signals.add("role_graph")
284
+ if any(role.get("archetype") for role in roles) or any(proposal.get("role_archetype") for proposal in proposals):
285
+ signals.add("archetype")
286
+ if proposals:
287
+ signals.update({"proposal", "candidate", "evaluation", "score", "stop"})
288
+ if diagnostics:
289
+ signals.add("diagnostic")
290
+ if search_paths or any(proposal.get("search_paths") for proposal in proposals):
291
+ signals.add("search_path")
292
+ if role_credit:
293
+ signals.add("credit")
294
+ governance_signals = {
295
+ _normalize(signal)
296
+ for signal in _as_list(governance.get("signals"))
297
+ if _normalize(signal)
298
+ }
299
+ if governance_signals or _as_list(governance.get("checks")):
300
+ signals.update({"governance", *governance_signals})
301
+ if best_candidate_id:
302
+ signals.add("best_candidate")
303
+ role_tokens = {
304
+ _normalize(proposal.get("role"))
305
+ for proposal in proposals
306
+ } | {
307
+ _normalize(proposal.get("role_kind"))
308
+ for proposal in proposals
309
+ }
310
+ if role_tokens & {"critic", "adversary", "vidura", "krishna"}:
311
+ signals.add("critique")
312
+ if role_tokens & {"synthesizer", "coverage_synthesis", "sangha"}:
313
+ signals.add("synthesis")
314
+ if role_tokens & {"steward", "dharma_steward"}:
315
+ signals.add("steward")
316
+ return signals
317
+
318
+
319
+ def _summary(
320
+ *,
321
+ roles: list[Mapping[str, Any]],
322
+ proposals: list[Mapping[str, Any]],
323
+ diagnostics: list[Mapping[str, Any]],
324
+ search_paths: list[str],
325
+ role_credit: list[Mapping[str, Any]],
326
+ best_candidate_id: str,
327
+ final_score: float,
328
+ rounds: list[Any],
329
+ governance: Mapping[str, Any],
330
+ ) -> Dict[str, Any]:
331
+ candidate_ids = [str(proposal.get("candidate_id") or "") for proposal in proposals if proposal.get("candidate_id")]
332
+ role_tokens = {
333
+ _normalize(proposal.get("role"))
334
+ for proposal in proposals
335
+ } | {
336
+ _normalize(proposal.get("role_kind"))
337
+ for proposal in proposals
338
+ }
339
+ governance_summary = dict(governance.get("summary") or {})
340
+ summary = {
341
+ "role_count": len(roles),
342
+ "proposal_count": len(proposals),
343
+ "evaluation_count": len(proposals),
344
+ "round_count": len(rounds) or len({proposal.get("round") for proposal in proposals if proposal.get("round") is not None}),
345
+ "diagnostic_count": len(diagnostics),
346
+ "search_path_count": len(search_paths),
347
+ "role_credit_count": len(role_credit),
348
+ "duplicate_candidate_count": max(0, len(candidate_ids) - len(set(candidate_ids))),
349
+ "best_candidate_id": best_candidate_id or None,
350
+ "final_score": final_score,
351
+ "has_role_graph": any(role.get("proposal_kind") for role in roles),
352
+ "has_critique": bool(role_tokens & {"critic", "adversary", "vidura", "krishna"}),
353
+ "has_synthesis": bool(role_tokens & {"synthesizer", "coverage_synthesis", "sangha"}),
354
+ "has_steward": bool(role_tokens & {"steward", "dharma_steward"}),
355
+ "terminal_status": "completed",
356
+ }
357
+ for key in (
358
+ "governance_check_count",
359
+ "governance_passed_count",
360
+ "governance_pass_rate",
361
+ "has_governance",
362
+ "has_role_diversity",
363
+ "has_mediator",
364
+ "has_contract_gate",
365
+ "has_rollback",
366
+ "has_locality",
367
+ "has_dependency_audit",
368
+ # Phase 4 society/contract flags (additive)
369
+ "has_guna_axes",
370
+ "has_two_chamber",
371
+ "has_nyaya_justifications",
372
+ "has_hetvabhasa_rejections",
373
+ "has_nirnaya",
374
+ "has_staged_conditioning",
375
+ "has_layer_locality",
376
+ "has_declared_budget",
377
+ "has_external_ranking",
378
+ ):
379
+ if key in governance_summary:
380
+ summary[key] = governance_summary[key]
381
+ return summary
382
+
383
+
384
+ def _governance_records(
385
+ *,
386
+ result_metadata: Mapping[str, Any],
387
+ roles: list[Mapping[str, Any]],
388
+ proposals: list[Mapping[str, Any]],
389
+ diagnostics: list[Mapping[str, Any]],
390
+ search_paths: list[str],
391
+ role_credit: list[Mapping[str, Any]],
392
+ best_candidate_id: str,
393
+ final_score: float,
394
+ ) -> Dict[str, Any]:
395
+ explicit = result_metadata.get("governance") or result_metadata.get("optimizer_governance")
396
+ explicit_checks = []
397
+ explicit_signals = []
398
+ if isinstance(explicit, Mapping):
399
+ explicit_checks = _as_list(explicit.get("checks"))
400
+ explicit_signals = _as_list(explicit.get("signals"))
401
+ elif explicit:
402
+ explicit_checks = _as_list(explicit)
403
+ explicit_checks.extend(_as_list(result_metadata.get("governance_checks")))
404
+
405
+ role_names = {
406
+ _normalize(role.get("name") or role.get("role"))
407
+ for role in roles
408
+ }
409
+ role_kinds = {
410
+ _normalize(role.get("proposal_kind"))
411
+ for role in roles
412
+ } | {
413
+ _normalize(proposal.get("role_kind"))
414
+ for proposal in proposals
415
+ }
416
+ proposal_roles = {
417
+ _normalize(proposal.get("role"))
418
+ for proposal in proposals
419
+ }
420
+ path_set = {str(path) for path in search_paths if str(path)}
421
+ patched_paths = {
422
+ str(path)
423
+ for proposal in proposals
424
+ for path in _as_list(proposal.get("search_paths"))
425
+ if str(path)
426
+ }
427
+ patched_paths.update(
428
+ str(path)
429
+ for proposal in proposals
430
+ for path in dict(proposal.get("patch") or {}).keys()
431
+ if str(path)
432
+ )
433
+ non_seed_roles = {role for role in proposal_roles if role and role not in {"seed", "unknown"}}
434
+ has_critique = bool((proposal_roles | role_kinds | role_names) & {"critic", "adversary", "vidura", "krishna"})
435
+ has_synthesis = bool((proposal_roles | role_kinds | role_names) & {"synthesizer", "coverage_synthesis", "sangha"})
436
+ has_steward = bool((proposal_roles | role_kinds | role_names) & {"steward", "dharma_steward"})
437
+ has_contract_path = any(
438
+ any(token in path for token in ("contract", "policy", "security", "safety", "guardrail"))
439
+ for path in path_set | patched_paths
440
+ )
441
+ has_dependency_audit = bool(
442
+ result_metadata.get("leave_one_backend_dependency")
443
+ or result_metadata.get("leave_one_backend_out")
444
+ or result_metadata.get("backend_lineage")
445
+ )
446
+ checks = [
447
+ _governance_check(
448
+ "role_diversity",
449
+ len(non_seed_roles) >= 3 or len(role_names) >= 3,
450
+ evidence={"roles": sorted(role_names | non_seed_roles)},
451
+ reason="multiple independent proposal roles reduce single-strategy collapse",
452
+ ),
453
+ _governance_check(
454
+ "topology_adaptation",
455
+ bool(role_kinds) or bool(result_metadata.get("role_graph")),
456
+ evidence={"role_kinds": sorted(kind for kind in role_kinds if kind)},
457
+ reason="role graph or role-kind metadata records the optimizer topology",
458
+ ),
459
+ _governance_check(
460
+ "adversarial_review",
461
+ has_critique,
462
+ evidence={"roles": sorted(role_names | proposal_roles | role_kinds)},
463
+ reason="critic or adversary role challenges candidate changes",
464
+ ),
465
+ _governance_check(
466
+ "mediator_review",
467
+ has_synthesis,
468
+ evidence={"roles": sorted(role_names | proposal_roles | role_kinds)},
469
+ reason="synthesis role combines compatible local repairs",
470
+ ),
471
+ _governance_check(
472
+ "steward_review",
473
+ has_steward,
474
+ evidence={"roles": sorted(role_names | proposal_roles | role_kinds)},
475
+ reason="steward role tests minimality and process safety",
476
+ ),
477
+ _governance_check(
478
+ "credit_assignment",
479
+ bool(role_credit),
480
+ evidence={"credit_roles": [str(item.get("role")) for item in role_credit]},
481
+ reason="role credit ledger connects outcomes to proposal sources",
482
+ ),
483
+ _governance_check(
484
+ "search_locality",
485
+ bool(path_set) and patched_paths.issubset(path_set),
486
+ evidence={"search_paths": sorted(path_set), "patched_paths": sorted(patched_paths)},
487
+ reason="candidate patches stay inside diagnosed search paths",
488
+ ),
489
+ _governance_check(
490
+ "contract_gate",
491
+ has_contract_path,
492
+ evidence={"search_paths": sorted(path_set), "diagnostics": diagnostics},
493
+ reason="policy/security/contract paths are tied to diagnosed failures",
494
+ ),
495
+ _governance_check(
496
+ "rollback_check",
497
+ has_steward or any(_as_list(proposal.get("parent_ids")) for proposal in proposals),
498
+ evidence={"has_steward": has_steward},
499
+ reason="steward or parent lineage supports rollback/minimality audit",
500
+ ),
501
+ _governance_check(
502
+ "terminal_selection",
503
+ bool(best_candidate_id) and final_score is not None,
504
+ evidence={"best_candidate_id": best_candidate_id, "final_score": final_score},
505
+ reason="trace names the selected candidate and final score",
506
+ ),
507
+ _governance_check(
508
+ "dependency_audit",
509
+ has_dependency_audit,
510
+ evidence={
511
+ "leave_one_backend_dependency": result_metadata.get("leave_one_backend_dependency"),
512
+ "backend_lineage": result_metadata.get("backend_lineage"),
513
+ },
514
+ reason="multi-backend runs should expose dependency or backend-lineage evidence",
515
+ ),
516
+ ]
517
+ # ---- Phase 4 additive governance records (conditional: emitted only when
518
+ # the producing metadata is present, so legacy traces keep their exact
519
+ # pre-Phase-4 check census). ----
520
+ rejection_records = [
521
+ dict(item)
522
+ for item in _as_list(result_metadata.get("rejections"))
523
+ if isinstance(item, Mapping)
524
+ ]
525
+ nirnaya_records = [
526
+ dict(item)
527
+ for item in _as_list(result_metadata.get("nirnaya"))
528
+ if isinstance(item, Mapping)
529
+ ]
530
+ selected_by_round: Dict[Any, set[str]] = {}
531
+ for record in nirnaya_records:
532
+ selected = record.get("selected_candidate_id")
533
+ if selected:
534
+ seen_round = selected_by_round.setdefault(record.get("round"), set())
535
+ seen_round.add(str(selected))
536
+ if len(seen_round) > 1:
537
+ raise ValueError(
538
+ "nirnaya records more than one selected candidate for round "
539
+ f"{record.get('round')!r}: selection is single-lineage and "
540
+ "proposals are never averaged."
541
+ )
542
+ justification = record.get("justification")
543
+ if justification is not None:
544
+ _validate_justification(justification)
545
+ chambers_meta = result_metadata.get("chambers")
546
+ chambers_meta = dict(chambers_meta) if isinstance(chambers_meta, Mapping) else None
547
+ ledger_round_records = [
548
+ dict(item)
549
+ for item in _as_list(result_metadata.get("ledger_rounds"))
550
+ if isinstance(item, Mapping)
551
+ ]
552
+ strategy_metadata = result_metadata.get("strategy_metadata")
553
+ strategy_metadata = (
554
+ dict(strategy_metadata) if isinstance(strategy_metadata, Mapping) else {}
555
+ )
556
+
557
+ proposal_author_counts: Dict[str, int] = {}
558
+ for proposal in proposals:
559
+ author = _normalize(proposal.get("role"))
560
+ if author and author not in {"seed", "unknown"}:
561
+ proposal_author_counts[author] = proposal_author_counts.get(author, 0) + 1
562
+ critique_operators: list[Dict[str, Any]] = []
563
+ for role in roles:
564
+ role_kind = _normalize(role.get("proposal_kind"))
565
+ critique_kind = _normalize(role.get("critique_kind")) or (
566
+ _CRITIQUE_KIND_BY_ROLE_KIND.get(role_kind, "")
567
+ )
568
+ if not critique_kind:
569
+ continue
570
+ role_name = _normalize(role.get("name") or role.get("role"))
571
+ authored = proposal_author_counts.get(role_name, 0)
572
+ operator: Dict[str, Any] = {
573
+ "role": str(role.get("name") or role.get("role") or ""),
574
+ "critique_kind": critique_kind,
575
+ "proposals_authored": authored,
576
+ }
577
+ if critique_kind == "vitanda" and authored:
578
+ # Refutation-only operators may reject; they must never appear as
579
+ # a proposal author.
580
+ operator["error"] = "vitanda_operator_authored_proposal"
581
+ critique_operators.append(operator)
582
+
583
+ role_prefixes = {
584
+ _normalize(role.get("name") or role.get("role")): [
585
+ str(prefix) for prefix in _as_list(role.get("path_prefixes")) if str(prefix)
586
+ ]
587
+ for role in roles
588
+ }
589
+ role_kinds_by_name = {
590
+ _normalize(role.get("name") or role.get("role")): _normalize(
591
+ role.get("proposal_kind")
592
+ )
593
+ for role in roles
594
+ }
595
+ authority_weights: list[Dict[str, Any]] = []
596
+ for proposal in proposals:
597
+ role_name = _normalize(proposal.get("role"))
598
+ prefixes = role_prefixes.get(role_name) or [
599
+ str(prefix)
600
+ for prefix in _as_list(
601
+ dict(proposal.get("metadata") or {}).get("role_path_prefixes")
602
+ )
603
+ if str(prefix)
604
+ ]
605
+ role_kind = role_kinds_by_name.get(role_name) or _normalize(
606
+ proposal.get("role_kind")
607
+ )
608
+ if role_kind != "specialist" and not prefixes:
609
+ continue
610
+ patch_paths = [str(path) for path in _as_list(proposal.get("search_paths"))]
611
+ in_scope = bool(prefixes) and all(
612
+ any(path == prefix or path.startswith(f"{prefix}.") for prefix in prefixes)
613
+ for path in patch_paths
614
+ )
615
+ authority_weights.append(
616
+ {
617
+ "candidate_id": proposal.get("candidate_id"),
618
+ "role": proposal.get("role"),
619
+ "weight": 1.0 if in_scope else 0.5,
620
+ "in_scope": in_scope,
621
+ }
622
+ )
623
+
624
+ if chambers_meta is not None and any(
625
+ isinstance(entry, Mapping) and entry.get("declared_budget") is not None
626
+ for entry in chambers_meta.values()
627
+ ):
628
+ checks.append(
629
+ _governance_check(
630
+ "chamber_budgets_declared",
631
+ all(
632
+ isinstance(entry, Mapping)
633
+ and entry.get("declared_budget") is not None
634
+ for entry in chambers_meta.values()
635
+ ),
636
+ evidence={"chambers": chambers_meta},
637
+ reason="every chamber declares its evaluation budget per round",
638
+ )
639
+ )
640
+ if rejection_records:
641
+ checks.append(
642
+ _governance_check(
643
+ "rejections_classed",
644
+ all(
645
+ str(record.get("hetvabhasa_class"))
646
+ in HETVABHASA_REJECTION_CLASSES
647
+ for record in rejection_records
648
+ ),
649
+ evidence={
650
+ "rejection_count": len(rejection_records),
651
+ "classes": sorted(
652
+ {
653
+ str(record.get("hetvabhasa_class"))
654
+ for record in rejection_records
655
+ }
656
+ ),
657
+ },
658
+ reason="every recorded rejection carries a closed-vocabulary class",
659
+ )
660
+ )
661
+ if nirnaya_records:
662
+ checks.append(
663
+ _governance_check(
664
+ "nirnaya_recorded",
665
+ all(
666
+ record.get("selected_candidate_id")
667
+ and isinstance(record.get("justification"), Mapping)
668
+ and all(
669
+ str(record["justification"].get(member) or "").strip()
670
+ for member in PANCA_AVAYAVA_MEMBERS
671
+ )
672
+ for record in nirnaya_records
673
+ ),
674
+ evidence={"nirnaya_count": len(nirnaya_records)},
675
+ reason="the steward decision is recorded with a complete justification",
676
+ )
677
+ )
678
+ checks.append(
679
+ _governance_check(
680
+ "proposals_never_averaged",
681
+ all(
682
+ isinstance(record.get("selected_candidate_id"), str)
683
+ and record.get("selected_candidate_id")
684
+ for record in nirnaya_records
685
+ )
686
+ and all(len(selected) == 1 for selected in selected_by_round.values()),
687
+ evidence={
688
+ "selected_candidates": sorted(
689
+ str(record.get("selected_candidate_id"))
690
+ for record in nirnaya_records
691
+ )
692
+ },
693
+ reason="selection is single-lineage: one decided candidate, never an average",
694
+ )
695
+ )
696
+ if authority_weights:
697
+ checks.append(
698
+ _governance_check(
699
+ "specialist_authority_respected",
700
+ all(
701
+ record["weight"] == (1.0 if record["in_scope"] else 0.5)
702
+ for record in authority_weights
703
+ ),
704
+ evidence={"authority_weight_count": len(authority_weights)},
705
+ reason=(
706
+ "specialist proposals inside their path prefixes carry full "
707
+ "authority; out-of-scope counter-proposals carry half"
708
+ ),
709
+ )
710
+ )
711
+ if ledger_round_records:
712
+ checks.append(
713
+ _governance_check(
714
+ "society_ledger_pooled_across_candidates",
715
+ any(
716
+ int(record.get("pooled_from_candidates") or 0) > 1
717
+ for record in ledger_round_records
718
+ ),
719
+ evidence={"ledger_rounds": ledger_round_records},
720
+ reason=(
721
+ "the round ledger pools diagnoses across all evaluated "
722
+ "candidates, not just the round winner"
723
+ ),
724
+ )
725
+ )
726
+
727
+ checks.extend(_normalize_governance_check(item) for item in explicit_checks)
728
+ checks = [check for check in checks if check]
729
+ seen: Dict[str, int] = {}
730
+ deduped_checks: list[Dict[str, Any]] = []
731
+ for check in checks:
732
+ name = _normalize(check.get("name"))
733
+ if not name:
734
+ continue
735
+ if name in seen:
736
+ existing_index = seen[name]
737
+ if check.get("passed") and not deduped_checks[existing_index].get("passed"):
738
+ deduped_checks[existing_index] = check
739
+ continue
740
+ seen[name] = len(deduped_checks)
741
+ deduped_checks.append(check)
742
+ signals = {
743
+ "governance",
744
+ *(_normalize(check.get("name")) for check in deduped_checks if check.get("passed")),
745
+ *(_normalize(signal) for signal in explicit_signals if _normalize(signal)),
746
+ }
747
+ passed_count = sum(1 for check in deduped_checks if check.get("passed"))
748
+ check_count = len(deduped_checks)
749
+
750
+ def _valid_guna(value: Any) -> bool:
751
+ if not isinstance(value, Mapping):
752
+ return False
753
+ if set(str(key) for key in value) != set(GUNA_AXES):
754
+ return False
755
+ try:
756
+ return all(0.0 <= float(value[axis]) <= 1.0 for axis in GUNA_AXES)
757
+ except (TypeError, ValueError):
758
+ return False
759
+
760
+ non_seed_proposals = [
761
+ proposal
762
+ for proposal in proposals
763
+ if _normalize(proposal.get("role")) not in {"seed", "unknown", ""}
764
+ ]
765
+ guna_mix = result_metadata.get("guna_mix") or strategy_metadata.get("guna_mix")
766
+ staged_conditioning = result_metadata.get(
767
+ "staged_conditioning"
768
+ ) or strategy_metadata.get("staged_conditioning")
769
+ layer_locality = result_metadata.get("layer_locality")
770
+ declared_chamber_budget = chambers_meta is not None and any(
771
+ isinstance(entry, Mapping) and entry.get("declared_budget") is not None
772
+ for entry in chambers_meta.values()
773
+ )
774
+ summary = {
775
+ "governance_check_count": check_count,
776
+ "governance_passed_count": passed_count,
777
+ "governance_pass_rate": round(passed_count / check_count, 4) if check_count else 0.0,
778
+ "has_governance": check_count > 0,
779
+ "has_role_diversity": _governance_passed(deduped_checks, "role_diversity"),
780
+ "has_mediator": _governance_passed(deduped_checks, "mediator_review"),
781
+ "has_contract_gate": _governance_passed(deduped_checks, "contract_gate"),
782
+ "has_rollback": _governance_passed(deduped_checks, "rollback_check"),
783
+ "has_locality": _governance_passed(deduped_checks, "search_locality"),
784
+ "has_dependency_audit": _governance_passed(deduped_checks, "dependency_audit"),
785
+ # ---- Phase 4 society/contract flags (additive) ----
786
+ "has_guna_axes": bool(roles)
787
+ and all(_valid_guna(role.get("guna")) for role in roles)
788
+ and isinstance(guna_mix, Mapping),
789
+ "has_two_chamber": bool(roles)
790
+ and all(str(role.get("chamber")) in CHAMBER_TOKENS for role in roles)
791
+ and chambers_meta is not None,
792
+ "has_nyaya_justifications": bool(non_seed_proposals)
793
+ and all(
794
+ isinstance(
795
+ dict(proposal.get("metadata") or {}).get("justification"), Mapping
796
+ )
797
+ and all(
798
+ str(
799
+ dict(proposal.get("metadata") or {})["justification"].get(member)
800
+ or ""
801
+ ).strip()
802
+ for member in PANCA_AVAYAVA_MEMBERS
803
+ )
804
+ for proposal in non_seed_proposals
805
+ ),
806
+ "has_hetvabhasa_rejections": bool(rejection_records)
807
+ and _governance_passed(deduped_checks, "rejections_classed"),
808
+ "has_nirnaya": bool(nirnaya_records)
809
+ and _governance_passed(deduped_checks, "nirnaya_recorded"),
810
+ "has_staged_conditioning": isinstance(staged_conditioning, Mapping)
811
+ and bool(staged_conditioning),
812
+ "has_layer_locality": bool(layer_locality)
813
+ or any(
814
+ str(diagnostic.get("harness_layer") or "")
815
+ for diagnostic in diagnostics
816
+ ),
817
+ "has_declared_budget": result_metadata.get("eval_budget") is not None
818
+ or declared_chamber_budget,
819
+ "has_external_ranking": str(result_metadata.get("ranking_source") or "")
820
+ in {"evaluation_suite", "evaluator"},
821
+ }
822
+ return {
823
+ "checks": deduped_checks,
824
+ "signals": sorted(signal for signal in signals if signal),
825
+ "summary": summary,
826
+ "nirnaya": nirnaya_records,
827
+ "critique_operators": critique_operators,
828
+ "authority_weights": authority_weights,
829
+ "rejections": rejection_records,
830
+ }
831
+
832
+
833
+ def _governance_check(
834
+ name: str,
835
+ passed: bool,
836
+ *,
837
+ evidence: Optional[Mapping[str, Any]] = None,
838
+ reason: str = "",
839
+ ) -> Dict[str, Any]:
840
+ return {
841
+ "name": name,
842
+ "passed": bool(passed),
843
+ "reason": reason,
844
+ "evidence": dict(evidence or {}),
845
+ }
846
+
847
+
848
+ def _normalize_governance_check(value: Any) -> Dict[str, Any]:
849
+ if isinstance(value, Mapping):
850
+ check = dict(value)
851
+ name = _normalize(check.get("name") or check.get("check") or check.get("signal"))
852
+ if not name:
853
+ return {}
854
+ return {
855
+ "name": name,
856
+ "passed": bool(check.get("passed", check.get("match", True))),
857
+ "reason": str(check.get("reason") or ""),
858
+ "evidence": dict(check.get("evidence") or {}),
859
+ }
860
+ name = _normalize(value)
861
+ if not name:
862
+ return {}
863
+ return {"name": name, "passed": True, "reason": "", "evidence": {}}
864
+
865
+
866
+ def _governance_passed(checks: Iterable[Mapping[str, Any]], name: str) -> bool:
867
+ normalized = _normalize(name)
868
+ return any(_normalize(check.get("name")) == normalized and bool(check.get("passed")) for check in checks)
869
+
870
+
871
+ def _as_list(value: Any) -> list[Any]:
872
+ if value is None:
873
+ return []
874
+ if isinstance(value, (list, tuple, set)):
875
+ return list(value)
876
+ return [value]
877
+
878
+
879
+ def _optional_float(value: Any) -> Optional[float]:
880
+ if value in (None, ""):
881
+ return None
882
+ try:
883
+ return float(value)
884
+ except (TypeError, ValueError):
885
+ return None
886
+
887
+
888
+ def _normalize(value: Any) -> str:
889
+ return str(value or "").strip().lower().replace("-", "_").replace(" ", "_").replace(".", "_")