ldcon 0.2.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 (482) hide show
  1. ldcon/__init__.py +302 -0
  2. ldcon/_moved.py +49 -0
  3. ldcon/agent_tools.py +4 -0
  4. ldcon/agents/__init__.py +1 -0
  5. ldcon/agents/adjustment_guidance.py +73 -0
  6. ldcon/agents/adjustment_quality.py +399 -0
  7. ldcon/agents/agent_adapters.py +709 -0
  8. ldcon/agents/agent_background.py +307 -0
  9. ldcon/agents/agent_bundle.py +75 -0
  10. ldcon/agents/agent_cards.py +171 -0
  11. ldcon/agents/agent_config_access.py +22 -0
  12. ldcon/agents/agent_configuration.py +224 -0
  13. ldcon/agents/agent_configuration_policy.py +67 -0
  14. ldcon/agents/agent_connect.py +93 -0
  15. ldcon/agents/agent_device_parameters.py +41 -0
  16. ldcon/agents/agent_execution.py +77 -0
  17. ldcon/agents/agent_history.py +112 -0
  18. ldcon/agents/agent_inbox_watch.py +121 -0
  19. ldcon/agents/agent_interactive.py +386 -0
  20. ldcon/agents/agent_interactive_hooks.py +381 -0
  21. ldcon/agents/agent_language.py +48 -0
  22. ldcon/agents/agent_log_context.py +308 -0
  23. ldcon/agents/agent_log_read.py +250 -0
  24. ldcon/agents/agent_log_watch.py +27 -0
  25. ldcon/agents/agent_metrics.py +111 -0
  26. ldcon/agents/agent_monitor.py +272 -0
  27. ldcon/agents/agent_opencode_http.py +270 -0
  28. ldcon/agents/agent_opencode_mcp.py +64 -0
  29. ldcon/agents/agent_owner_status.py +216 -0
  30. ldcon/agents/agent_profiles.py +186 -0
  31. ldcon/agents/agent_providers.py +53 -0
  32. ldcon/agents/agent_repairs.py +105 -0
  33. ldcon/agents/agent_routing.py +90 -0
  34. ldcon/agents/agent_runtime.py +70 -0
  35. ldcon/agents/agent_sandbox.py +141 -0
  36. ldcon/agents/agent_startup_notice.py +142 -0
  37. ldcon/agents/agent_task.py +336 -0
  38. ldcon/agents/agent_tools.py +488 -0
  39. ldcon/agents/agent_window.py +70 -0
  40. ldcon/agents/agent_workspace.py +340 -0
  41. ldcon/agents/card_fallback.py +284 -0
  42. ldcon/agents/dlc_pi_adjustment.py +250 -0
  43. ldcon/agents/dlc_pi_plan.py +24 -0
  44. ldcon/agents/dlc_pi_response_plan.py +26 -0
  45. ldcon/agents/failure_policy.py +473 -0
  46. ldcon/agents/fallback_service.py +146 -0
  47. ldcon/agents/fallback_targets.py +96 -0
  48. ldcon/agents/gateway_role.py +31 -0
  49. ldcon/agents/manual_adjustment_grant.py +66 -0
  50. ldcon/agents/measurement_grant.py +95 -0
  51. ldcon/agents/monitor_quality_binding.py +33 -0
  52. ldcon/agents/notification_policy.py +346 -0
  53. ldcon/agents/operating_mode_client.py +99 -0
  54. ldcon/agents/report_pdf.py +105 -0
  55. ldcon/agents/resident_adjustment.py +572 -0
  56. ldcon/agents/resident_agent.py +438 -0
  57. ldcon/agents/resident_quality_apply.py +233 -0
  58. ldcon/agents/task_decisions.py +218 -0
  59. ldcon/agents/teams_card.py +100 -0
  60. ldcon/agents/teams_destinations.py +157 -0
  61. ldcon/cld_owner.py +4 -0
  62. ldcon/cli.py +19 -0
  63. ldcon/core/__init__.py +1 -0
  64. ldcon/core/adjustment_plan.py +47 -0
  65. ldcon/core/adjustment_registry.py +100 -0
  66. ldcon/core/agent_authority.py +903 -0
  67. ldcon/core/card_admission.py +15 -0
  68. ldcon/core/card_definition.py +14 -0
  69. ldcon/core/card_reporting.py +238 -0
  70. ldcon/core/configuration.py +357 -0
  71. ldcon/core/configuration_transaction.py +225 -0
  72. ldcon/core/contracts.py +86 -0
  73. ldcon/core/current_settings.py +86 -0
  74. ldcon/core/deck_yaml.py +45 -0
  75. ldcon/core/device_conditions.py +133 -0
  76. ldcon/core/execution_context.py +3 -0
  77. ldcon/core/instrument_client.py +190 -0
  78. ldcon/core/interface_cards.py +520 -0
  79. ldcon/core/interface_schema.py +640 -0
  80. ldcon/core/laser_assets.py +238 -0
  81. ldcon/core/metric_quantities.py +38 -0
  82. ldcon/core/modulation.py +107 -0
  83. ldcon/core/operation_registry.py +195 -0
  84. ldcon/core/paths.py +26 -0
  85. ldcon/core/period_context.py +118 -0
  86. ldcon/core/records.py +4 -0
  87. ldcon/core/result_reservations.py +469 -0
  88. ldcon/core/site_bindings.py +25 -0
  89. ldcon/core/state_layout.py +70 -0
  90. ldcon/core/target_policy.py +79 -0
  91. ldcon/core/telegraf.py +106 -0
  92. ldcon/core/telemetry.py +130 -0
  93. ldcon/core/temperature_settings.py +12 -0
  94. ldcon/dashboard_metrics.py +4 -0
  95. ldcon/device_registry.py +4 -0
  96. ldcon/drivers/__init__.py +1 -0
  97. ldcon/drivers/cld1015.py +296 -0
  98. ldcon/drivers/cld1015_config.py +31 -0
  99. ldcon/drivers/dlcpro.py +177 -0
  100. ldcon/drivers/dlcpro_analog.py +33 -0
  101. ldcon/drivers/dlcpro_console.py +133 -0
  102. ldcon/drivers/dlcpro_priority.py +46 -0
  103. ldcon/drivers/ecdl.py +334 -0
  104. ldcon/drivers/esp32_servo.py +187 -0
  105. ldcon/drivers/fizeau.py +114 -0
  106. ldcon/drivers/frequency_sweep.py +69 -0
  107. ldcon/drivers/influx_wavelength.py +70 -0
  108. ldcon/drivers/metric_sources.py +82 -0
  109. ldcon/drivers/native_dither_telemetry.py +50 -0
  110. ldcon/drivers/rigol_scope.py +126 -0
  111. ldcon/drivers/siglent.py +418 -0
  112. ldcon/drivers/staircase.py +155 -0
  113. ldcon/drivers/wsu_udp.py +164 -0
  114. ldcon/esp32_time_telemetry.py +4 -0
  115. ldcon/example_config.py +41 -0
  116. ldcon/fallback_service.py +4 -0
  117. ldcon/instrument_owner.py +4 -0
  118. ldcon/instrument_service.py +4 -0
  119. ldcon/interface_worker.py +4 -0
  120. ldcon/job_service.py +4 -0
  121. ldcon/jobs/__init__.py +1 -0
  122. ldcon/jobs/adaptive_pid.py +234 -0
  123. ldcon/jobs/adaptive_window_session.py +922 -0
  124. ldcon/jobs/adjustment_continuation.py +284 -0
  125. ldcon/jobs/axis_frequency.py +219 -0
  126. ldcon/jobs/axis_frequency_report.py +86 -0
  127. ldcon/jobs/background_parameter_session.py +142 -0
  128. ldcon/jobs/background_pid_session.py +447 -0
  129. ldcon/jobs/branch_maps.py +71 -0
  130. ldcon/jobs/branch_target_migration.py +33 -0
  131. ldcon/jobs/card_runtime.py +157 -0
  132. ldcon/jobs/change_measure.py +293 -0
  133. ldcon/jobs/change_measure_adapter.py +360 -0
  134. ldcon/jobs/change_measure_dlc.py +94 -0
  135. ldcon/jobs/change_measure_parameters.py +222 -0
  136. ldcon/jobs/change_measure_target.py +135 -0
  137. ldcon/jobs/current_cards.py +159 -0
  138. ldcon/jobs/current_devices.py +71 -0
  139. ldcon/jobs/dashboard_api.py +61 -0
  140. ldcon/jobs/device_parameters.py +426 -0
  141. ldcon/jobs/dlc_pi_measurement.py +151 -0
  142. ldcon/jobs/dlcpro_awg_analysis.py +77 -0
  143. ldcon/jobs/dlcpro_awg_card.py +116 -0
  144. ldcon/jobs/dlcpro_cards.py +240 -0
  145. ldcon/jobs/ecdl_branch_hop.py +175 -0
  146. ldcon/jobs/ecdl_branch_recovery.py +176 -0
  147. ldcon/jobs/ecdl_devices.py +306 -0
  148. ldcon/jobs/ecdl_locked_sweep_proxy.py +526 -0
  149. ldcon/jobs/ecdl_mapped_recovery.py +6 -0
  150. ldcon/jobs/ecdl_mode_map.py +469 -0
  151. ldcon/jobs/ecdl_mode_recovery.py +20 -0
  152. ldcon/jobs/ecdl_online.py +158 -0
  153. ldcon/jobs/ecdl_operating_point.py +403 -0
  154. ldcon/jobs/ecdl_pi_commission.py +161 -0
  155. ldcon/jobs/ecdl_recovery.py +25 -0
  156. ldcon/jobs/ecdl_recovery_plan.py +90 -0
  157. ldcon/jobs/ecdl_recovery_test.py +59 -0
  158. ldcon/jobs/ecdl_relock.py +7 -0
  159. ldcon/jobs/ecdl_response_study.py +369 -0
  160. ldcon/jobs/ecdl_steady.py +343 -0
  161. ldcon/jobs/ecdl_thermal_context.py +242 -0
  162. ldcon/jobs/ecdl_window_center.py +734 -0
  163. ldcon/jobs/emission_card.py +102 -0
  164. ldcon/jobs/fringe_sweep_reference.py +89 -0
  165. ldcon/jobs/interface_devices.py +122 -0
  166. ldcon/jobs/interface_worker.py +471 -0
  167. ldcon/jobs/job_service.py +125 -0
  168. ldcon/jobs/laser_live.py +270 -0
  169. ldcon/jobs/learned_settling.py +450 -0
  170. ldcon/jobs/lock_target.py +461 -0
  171. ldcon/jobs/locked_triangle.py +36 -0
  172. ldcon/jobs/measurement_acquisition.py +51 -0
  173. ldcon/jobs/measurement_analysis.py +59 -0
  174. ldcon/jobs/measurement_execution.py +40 -0
  175. ldcon/jobs/measurement_notice.py +78 -0
  176. ldcon/jobs/measurement_presets.py +52 -0
  177. ldcon/jobs/measurement_report.py +76 -0
  178. ldcon/jobs/measurement_statistics.py +22 -0
  179. ldcon/jobs/metric_cards.py +34 -0
  180. ldcon/jobs/metric_interface.py +100 -0
  181. ldcon/jobs/modulation_sweep.py +274 -0
  182. ldcon/jobs/native_background_session.py +91 -0
  183. ldcon/jobs/native_current_acquire.py +143 -0
  184. ldcon/jobs/native_current_card.py +328 -0
  185. ldcon/jobs/native_current_online.py +129 -0
  186. ldcon/jobs/native_ff_proxy.py +1361 -0
  187. ldcon/jobs/native_ff_sweep.py +802 -0
  188. ldcon/jobs/online_pid.py +178 -0
  189. ldcon/jobs/operating_point.py +1399 -0
  190. ldcon/jobs/operating_point_proxy.py +236 -0
  191. ldcon/jobs/operation_execution.py +126 -0
  192. ldcon/jobs/operation_history.py +419 -0
  193. ldcon/jobs/optical_broker.py +23 -0
  194. ldcon/jobs/pi_algorithms.py +30 -0
  195. ldcon/jobs/pi_response.py +239 -0
  196. ldcon/jobs/progress.py +63 -0
  197. ldcon/jobs/quality_baseline.py +411 -0
  198. ldcon/jobs/recovery_decisions.py +131 -0
  199. ldcon/jobs/recovery_history.py +152 -0
  200. ldcon/jobs/recovery_procedure.py +285 -0
  201. ldcon/jobs/recovery_reference.py +123 -0
  202. ldcon/jobs/recovery_stationary.py +138 -0
  203. ldcon/jobs/response_algorithms.py +47 -0
  204. ldcon/jobs/response_comparison.py +62 -0
  205. ldcon/jobs/response_record.py +90 -0
  206. ldcon/jobs/runtime_parameters.py +434 -0
  207. ldcon/jobs/settling_profile.py +140 -0
  208. ldcon/jobs/shot_analysis.py +1300 -0
  209. ldcon/jobs/simulation.py +127 -0
  210. ldcon/jobs/slow_control.py +208 -0
  211. ldcon/jobs/slow_current.py +88 -0
  212. ldcon/jobs/study_adoption.py +122 -0
  213. ldcon/jobs/study_execution.py +182 -0
  214. ldcon/jobs/sweep_ff_analysis.py +320 -0
  215. ldcon/jobs/sweep_ff_optimization.py +202 -0
  216. ldcon/jobs/sweep_ff_plan.py +166 -0
  217. ldcon/jobs/sweep_ff_report.py +254 -0
  218. ldcon/jobs/sweep_ff_transport.py +326 -0
  219. ldcon/jobs/sweep_tracking.py +424 -0
  220. ldcon/jobs/sweep_verdict.py +288 -0
  221. ldcon/jobs/target_response.py +638 -0
  222. ldcon/jobs/target_response_analysis.py +65 -0
  223. ldcon/jobs/target_response_control.py +215 -0
  224. ldcon/jobs/target_response_decisions.py +152 -0
  225. ldcon/jobs/target_response_experiment.py +110 -0
  226. ldcon/jobs/target_response_feedforward.py +130 -0
  227. ldcon/jobs/target_response_search.py +225 -0
  228. ldcon/jobs/target_response_shift.py +475 -0
  229. ldcon/jobs/target_response_thermal.py +113 -0
  230. ldcon/jobs/target_response_thermal_experiment.py +120 -0
  231. ldcon/jobs/target_sequence.py +166 -0
  232. ldcon/jobs/temperature_identification.py +86 -0
  233. ldcon/jobs/wavelength_history.py +17 -0
  234. ldcon/jobs/window_actions.py +641 -0
  235. ldcon/jobs/window_center.py +784 -0
  236. ldcon/jobs/window_control.py +130 -0
  237. ldcon/jobs/window_decisions.py +105 -0
  238. ldcon/jobs/window_sampling.py +66 -0
  239. ldcon/jobs/workflow_artifacts.py +73 -0
  240. ldcon/migration/__init__.py +810 -0
  241. ldcon/migration/migration_decks.py +239 -0
  242. ldcon/migration/migration_readiness.py +397 -0
  243. ldcon/migration/migration_relocation.py +283 -0
  244. ldcon/monitor_owner.py +4 -0
  245. ldcon/owner_telemetry.py +4 -0
  246. ldcon/owners/__init__.py +1 -0
  247. ldcon/owners/adc_log_threshold.py +209 -0
  248. ldcon/owners/background_grants.py +44 -0
  249. ldcon/owners/cld_instrument_owner.py +96 -0
  250. ldcon/owners/cld_owner.py +706 -0
  251. ldcon/owners/cld_projection.py +194 -0
  252. ldcon/owners/cld_system_owner.py +276 -0
  253. ldcon/owners/current_owner.py +68 -0
  254. ldcon/owners/device_registry.py +229 -0
  255. ldcon/owners/dlcpro_awg_plan.py +115 -0
  256. ldcon/owners/dlcpro_awg_port.py +98 -0
  257. ldcon/owners/dlcpro_awg_protocol.py +190 -0
  258. ldcon/owners/dlcpro_awg_return.py +63 -0
  259. ldcon/owners/dlcpro_awg_sweep.py +437 -0
  260. ldcon/owners/dlcpro_awg_timing.py +35 -0
  261. ldcon/owners/dlcpro_awg_tracking.py +175 -0
  262. ldcon/owners/dlcpro_awg_tracking_guard.py +87 -0
  263. ldcon/owners/dlcpro_awg_tracking_report.py +49 -0
  264. ldcon/owners/dlcpro_awg_zero.py +82 -0
  265. ldcon/owners/dlcpro_deployment.py +66 -0
  266. ldcon/owners/dlcpro_ff.py +64 -0
  267. ldcon/owners/dlcpro_ff_adjustment.py +159 -0
  268. ldcon/owners/dlcpro_ff_delta_adjustment.py +194 -0
  269. ldcon/owners/dlcpro_frequency_window.py +61 -0
  270. ldcon/owners/dlcpro_observation.py +62 -0
  271. ldcon/owners/dlcpro_owner.py +830 -0
  272. ldcon/owners/dlcpro_pi_protocol.py +44 -0
  273. ldcon/owners/dlcpro_pi_trial.py +161 -0
  274. ldcon/owners/dlcpro_readback.py +50 -0
  275. ldcon/owners/dlcpro_service.py +203 -0
  276. ldcon/owners/dlcpro_target_changes.py +330 -0
  277. ldcon/owners/dlcpro_target_step.py +44 -0
  278. ldcon/owners/dlcpro_target_store.py +75 -0
  279. ldcon/owners/dlcpro_telemetry.py +72 -0
  280. ldcon/owners/ecdl_background_parameters.py +248 -0
  281. ldcon/owners/ecdl_background_pid.py +184 -0
  282. ldcon/owners/ecdl_hot_control.py +255 -0
  283. ldcon/owners/ecdl_independent_pid.py +95 -0
  284. ldcon/owners/ecdl_locked_study.py +122 -0
  285. ldcon/owners/ecdl_locked_sweep.py +1875 -0
  286. ldcon/owners/ecdl_native_pid.py +181 -0
  287. ldcon/owners/ecdl_optical.py +37 -0
  288. ldcon/owners/ecdl_owner.py +1058 -0
  289. ldcon/owners/ecdl_policy_reload.py +494 -0
  290. ldcon/owners/ecdl_prelock.py +612 -0
  291. ldcon/owners/ecdl_projection.py +636 -0
  292. ldcon/owners/ecdl_steady_owner.py +1859 -0
  293. ldcon/owners/ecdl_steady_policy.py +212 -0
  294. ldcon/owners/ecdl_target_ff.py +214 -0
  295. ldcon/owners/ecdl_target_settling.py +163 -0
  296. ldcon/owners/ecdl_tec_pair.py +88 -0
  297. ldcon/owners/ecdl_thermal_study.py +251 -0
  298. ldcon/owners/emission.py +365 -0
  299. ldcon/owners/emission_adapters.py +268 -0
  300. ldcon/owners/esp32_native_client.py +335 -0
  301. ldcon/owners/esp32_native_owner.py +2166 -0
  302. ldcon/owners/esp32_time_telemetry.py +253 -0
  303. ldcon/owners/esp32_udp_gate.py +136 -0
  304. ldcon/owners/external_sweep_guard.py +44 -0
  305. ldcon/owners/flexible_native_trial.py +172 -0
  306. ldcon/owners/fringe_peaks.py +92 -0
  307. ldcon/owners/full_data_log.py +133 -0
  308. ldcon/owners/instrument_identity.py +125 -0
  309. ldcon/owners/instrument_owner.py +1041 -0
  310. ldcon/owners/instrument_projection.py +104 -0
  311. ldcon/owners/instrument_service.py +638 -0
  312. ldcon/owners/migration_physical_lock.py +162 -0
  313. ldcon/owners/monitor_owner.py +322 -0
  314. ldcon/owners/native_assembly.py +37 -0
  315. ldcon/owners/native_assembly_host.py +260 -0
  316. ldcon/owners/native_background.py +233 -0
  317. ldcon/owners/native_current_tuning.py +251 -0
  318. ldcon/owners/native_ff_proof.py +433 -0
  319. ldcon/owners/native_instrument_owner.py +161 -0
  320. ldcon/owners/native_modulation_limits.py +45 -0
  321. ldcon/owners/native_policy_reload.py +186 -0
  322. ldcon/owners/native_policy_safety.py +213 -0
  323. ldcon/owners/native_policy_update.py +473 -0
  324. ldcon/owners/native_system_owner.py +420 -0
  325. ldcon/owners/operating_mode_owner.py +128 -0
  326. ldcon/owners/owner_projection.py +530 -0
  327. ldcon/owners/owner_telemetry.py +166 -0
  328. ldcon/owners/pi_handover.py +154 -0
  329. ldcon/owners/projection_transport.py +143 -0
  330. ldcon/owners/remote_instruments.py +323 -0
  331. ldcon/owners/sdg_client.py +122 -0
  332. ldcon/owners/sdg_owner.py +468 -0
  333. ldcon/owners/tec_rate.py +29 -0
  334. ldcon/owners/wsu_udp_monitor.py +113 -0
  335. ldcon/report_pdf.py +4 -0
  336. ldcon/resources/agent-model-options.json +142 -0
  337. ldcon/resources/agent_contract/adjustment.txt +45 -0
  338. ldcon/resources/agent_contract/common.txt +42 -0
  339. ldcon/resources/agent_contract/diagnosis.txt +11 -0
  340. ldcon/resources/agent_contract/research.txt +20 -0
  341. ldcon/resources/agent_contract/response_optimization.txt +13 -0
  342. ldcon/resources/examples/README.html +10 -0
  343. ldcon/resources/examples/assignments/sr1092.yaml +100 -0
  344. ldcon/resources/examples/assignments/sr422.yaml +677 -0
  345. ldcon/resources/examples/assignments/thf685.yaml +7 -0
  346. ldcon/resources/examples/interface-decks/dfb-dbr-ldc-tec.yaml +430 -0
  347. ldcon/resources/examples/interface-decks/ecdl-dlcpro.yaml +288 -0
  348. ldcon/resources/examples/interface-decks/ecdl-pzt-ldc-tec.yaml +698 -0
  349. ldcon/resources/examples/lasers/sr1092.yaml +267 -0
  350. ldcon/resources/examples/lasers/sr422.yaml +403 -0
  351. ldcon/resources/examples/lasers/thf685.yaml +158 -0
  352. ldcon/resources/report_template/ldconreport.cls +42 -0
  353. ldcon/resources/report_template/report.tex +47 -0
  354. ldcon/web/__init__.py +1 -0
  355. ldcon/web/agent_approval_api.py +126 -0
  356. ldcon/web/agent_settings_api.py +54 -0
  357. ldcon/web/agent_task_api.py +211 -0
  358. ldcon/web/control_readback.py +63 -0
  359. ldcon/web/dashboard_activity.py +109 -0
  360. ldcon/web/dashboard_controls_revision.py +29 -0
  361. ldcon/web/dashboard_metrics.py +146 -0
  362. ldcon/web/dashboard_snapshot.py +163 -0
  363. ldcon/web/dashboard_state.py +104 -0
  364. ldcon/web/dashboard_wavelength.py +150 -0
  365. ldcon/web/device_parameters_api.py +52 -0
  366. ldcon/web/dither_quality.py +46 -0
  367. ldcon/web/dlcpro_target_api.py +91 -0
  368. ldcon/web/dlcpro_target_query.py +23 -0
  369. ldcon/web/failure_api.py +62 -0
  370. ldcon/web/gateway_lan.py +208 -0
  371. ldcon/web/grafana_activity.py +84 -0
  372. ldcon/web/grafana_dashboard.py +492 -0
  373. ldcon/web/grafana_dlcpro.py +158 -0
  374. ldcon/web/grafana_quality.py +167 -0
  375. ldcon/web/interface_projection.py +27 -0
  376. ldcon/web/job_registry.py +68 -0
  377. ldcon/web/job_report.py +122 -0
  378. ldcon/web/laser_operations.py +445 -0
  379. ldcon/web/lock_target_api.py +554 -0
  380. ldcon/web/lock_target_queue.py +212 -0
  381. ldcon/web/log_archive.py +191 -0
  382. ldcon/web/metric_query_profiles.py +378 -0
  383. ldcon/web/online_position.py +26 -0
  384. ldcon/web/operating_mode_api.py +72 -0
  385. ldcon/web/operation_api.py +34 -0
  386. ldcon/web/operation_observation.py +57 -0
  387. ldcon/web/operations_settings.py +92 -0
  388. ldcon/web/quality_threshold_metrics.py +64 -0
  389. ldcon/web/shot_analysis_api.py +136 -0
  390. ldcon/web/sweep_api.py +1186 -0
  391. ldcon/web/sweep_guard.py +45 -0
  392. ldcon/web/target_reference_panel.py +38 -0
  393. ldcon/web/teams_destinations_api.py +81 -0
  394. ldcon/web/web_fleet.py +482 -0
  395. ldcon/web/web_runtime.py +811 -0
  396. ldcon/web/web_server.py +402 -0
  397. ldcon/web/window_api.py +28 -0
  398. ldcon/web_assets/agent-settings.css +4 -0
  399. ldcon/web_assets/agent-settings.html +53 -0
  400. ldcon/web_assets/agent-settings.js +270 -0
  401. ldcon/web_assets/app.css +9 -0
  402. ldcon/web_assets/app.js +802 -0
  403. ldcon/web_assets/card-browser.js +160 -0
  404. ldcon/web_assets/card-guide.js +9 -0
  405. ldcon/web_assets/cards/acquire.metrics.html +82 -0
  406. ldcon/web_assets/cards/agent.history_review.html +13 -0
  407. ldcon/web_assets/cards/agent.log_watch.html +1 -0
  408. ldcon/web_assets/cards/agent.monitor.html +13 -0
  409. ldcon/web_assets/cards/agent.request.html +1 -0
  410. ldcon/web_assets/cards/agent.resident.html +1 -0
  411. ldcon/web_assets/cards/agent.task.html +17 -0
  412. ldcon/web_assets/cards/commission.ecdl_pi.html +1 -0
  413. ldcon/web_assets/cards/identify.axis_frequency.html +1 -0
  414. ldcon/web_assets/cards/identify.current_response.html +1 -0
  415. ldcon/web_assets/cards/laser.emission.html +8 -0
  416. ldcon/web_assets/cards/measure.batch.html +1 -0
  417. ldcon/web_assets/cards/measure.change.html +1 -0
  418. ldcon/web_assets/cards/measure.target_response.html +1 -0
  419. ldcon/web_assets/cards/monitor.wavelength.html +1 -0
  420. ldcon/web_assets/cards/notify.teams.html +1 -0
  421. ldcon/web_assets/cards/optimize.settling_criteria.html +1 -0
  422. ldcon/web_assets/cards/optimize.target_feedforward.html +1 -0
  423. ldcon/web_assets/cards/optimize.target_step.html +1 -0
  424. ldcon/web_assets/cards/optimize.target_thermal.html +1 -0
  425. ldcon/web_assets/cards/plan.ecdl_recovery.html +14 -0
  426. ldcon/web_assets/cards/prepare.ecdl_recovery.html +18 -0
  427. ldcon/web_assets/cards/recover.ecdl_mode_hop.html +12 -0
  428. ldcon/web_assets/cards/report.grafana_dashboard.html +22 -0
  429. ldcon/web_assets/cards/servo.current_pid.html +1 -0
  430. ldcon/web_assets/cards/servo.dlcpro_frequency.html +1 -0
  431. ldcon/web_assets/cards/servo.ecdl_steady_lock.html +11 -0
  432. ldcon/web_assets/cards/servo.native_current.html +1 -0
  433. ldcon/web_assets/cards/study.ecdl_current_response.html +1 -0
  434. ldcon/web_assets/cards/study.ecdl_locked_current_map.html +1 -0
  435. ldcon/web_assets/cards/study.ecdl_mode_map.html +6 -0
  436. ldcon/web_assets/cards/study.ecdl_thermal_context.html +5 -0
  437. ldcon/web_assets/cards/study.target_response.html +1 -0
  438. ldcon/web_assets/cards/sweep.current_modulation.html +1 -0
  439. ldcon/web_assets/cards/sweep.dlcpro_frequency.html +1 -0
  440. ldcon/web_assets/cards/sweep.servo_modulation.html +1 -0
  441. ldcon/web_assets/cards/test.ecdl_recovery.html +4 -0
  442. ldcon/web_assets/cards/tune.ecdl_initial_point.html +1 -0
  443. ldcon/web_assets/cards/tune.ecdl_operating_point.html +13 -0
  444. ldcon/web_assets/cards/tune.ecdl_window_center.html +7 -0
  445. ldcon/web_assets/cards/tune.native_current.html +1 -0
  446. ldcon/web_assets/cards/tune.pid_coordinate_search.html +1 -0
  447. ldcon/web_assets/cards/tune.pid_factor_reduction.html +1 -0
  448. ldcon/web_assets/cards/tune.pid_online.html +1 -0
  449. ldcon/web_assets/cards/tune.temperature_once.html +18 -0
  450. ldcon/web_assets/committing-input.js +35 -0
  451. ldcon/web_assets/configuration-editor.js +194 -0
  452. ldcon/web_assets/dashboard-state.js +39 -0
  453. ldcon/web_assets/failures.html +1 -0
  454. ldcon/web_assets/failures.js +19 -0
  455. ldcon/web_assets/fallback.css +3 -0
  456. ldcon/web_assets/fallback.html +1 -0
  457. ldcon/web_assets/fallback.js +21 -0
  458. ldcon/web_assets/fleet-settings.js +20 -0
  459. ldcon/web_assets/fleet.css +9 -0
  460. ldcon/web_assets/fleet.html +8 -0
  461. ldcon/web_assets/fleet.js +130 -0
  462. ldcon/web_assets/gateway-presentation.js +27 -0
  463. ldcon/web_assets/grafana-help.js +31 -0
  464. ldcon/web_assets/help.html +22 -0
  465. ldcon/web_assets/i18n.js +279 -0
  466. ldcon/web_assets/index.html +14 -0
  467. ldcon/web_assets/laser-workspace.js +279 -0
  468. ldcon/web_assets/modulation.html +1 -0
  469. ldcon/web_assets/modulation.js +14 -0
  470. ldcon/web_assets/reservations.html +16 -0
  471. ldcon/web_assets/reservations.js +77 -0
  472. ldcon/web_assets/teams-settings.js +91 -0
  473. ldcon/web_assets/window-actions.js +37 -0
  474. ldcon/web_assets/workspace.css +99 -0
  475. ldcon/web_fleet.py +4 -0
  476. ldcon/web_server.py +4 -0
  477. ldcon-0.2.0.dist-info/METADATA +85 -0
  478. ldcon-0.2.0.dist-info/RECORD +482 -0
  479. ldcon-0.2.0.dist-info/WHEEL +5 -0
  480. ldcon-0.2.0.dist-info/entry_points.txt +5 -0
  481. ldcon-0.2.0.dist-info/licenses/LICENSE +21 -0
  482. ldcon-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,399 @@
1
+ """Evidence-bound operational quality candidates; this module never saves settings.
2
+
3
+ An owner's measured result is authoritative about acquisition validation. Model
4
+ assessments remain labelled as assessments, and old classification thresholds and
5
+ raw results are preserved even when a more permissive candidate is proposed.
6
+
7
+ Revisable thresholds (2026-10-28): the optical ``single_mode_max`` scalar leaves
8
+ and the lock device's dither quality limit
9
+ ``steady_lock.window.warning_response`` (the monitor ``dither_response``
10
+ condition and the dashboard quality line). The dither limit is judged against
11
+ the same repeated post-settle dither fits that the evidence rules already
12
+ require, aggregated as their mean (``dither_aggregate``; 2026-10-28, a single
13
+ 32 s fit scatters by about 15-20 % window to window, and the owner trim guard
14
+ and the monitor's 300 s persistence aggregate too); the optical leaves keep the
15
+ maximum rule. ``window.edge_response`` is a window-search experiment boundary and is
16
+ never a quality threshold here; safety keys are never in scope.
17
+ """
18
+ import copy
19
+ import json
20
+ import math
21
+ import statistics
22
+ import time
23
+
24
+ from experiment_core.records import atomic_json, digest
25
+ from ldcon.agents.agent_configuration import QUALITY_MAPS, QUALITY_METRICS, DITHER_QUALITY_LEAF
26
+
27
+ REVISION = 'unified-quality-2'
28
+ FIELDS = frozenset(('selected_current', 'measurement_ids', 'classification',
29
+ 'margin_fraction', 'history_assessment', 'rationale',
30
+ 'follow_up', 'changes'))
31
+ # Owner result states that are receipts of no output write (2026-10-28):
32
+ # ``stale_decision`` is returned before ``_start`` runs; ``prewrite_rejected`` is
33
+ # returned before any move or when the owner reports ``applied`` is not true.
34
+ # ``rejected`` is excluded because a late validation error can follow a move.
35
+ NO_WRITE_STATES = frozenset(('prewrite_rejected', 'stale_decision'))
36
+ HISTORY_FIELDS = frozenset(('evidence_refs', 'interval_unix_s', 'sample_coverage',
37
+ 'comparability', 'drift_and_interventions', 'practical_requirement',
38
+ 'unmeasured_alternatives'))
39
+
40
+
41
+ DITHER_SUFFIX = '.' + '.'.join(DITHER_QUALITY_LEAF)
42
+ DITHER_AGGREGATION = 'mean_of_valid_fits'
43
+ DITHER_RULE = 'mean of valid post-settle dither fits at the selected point <= limit*(1-margin_fraction)'
44
+
45
+
46
+ def finite(value):
47
+ return type(value) in (int, float) and math.isfinite(value)
48
+
49
+
50
+ def no_write(row):
51
+ """An owner receipt that itself records no measurement and no applied move."""
52
+ receipt = row.get('receipt')
53
+ return (not any(k in row for k in ('optical_frames', 'target', 'adoption', 'restoration'))
54
+ and (receipt is None or (isinstance(receipt, dict) and receipt.get('applied') is not True)))
55
+
56
+
57
+ def is_dither(path):
58
+ return path.endswith(DITHER_SUFFIX)
59
+
60
+
61
+ def dither_aggregate(fits):
62
+ """Mean rule for the dither quality limit: n, mean, max, min and sample std of valid fits."""
63
+ values = [fit['value'] for fit in fits]
64
+ return dict(aggregation=DITHER_AGGREGATION, rule=DITHER_RULE, n=len(values),
65
+ mean=statistics.fmean(values), max=max(values), min=min(values),
66
+ std=statistics.stdev(values) if len(values) > 1 else None,
67
+ std_kind='sample standard deviation (n-1)', fits=copy.deepcopy(fits))
68
+
69
+
70
+ def dither_meets(aggregate, limit, margin):
71
+ return aggregate['mean'] <= limit*(1-margin)
72
+
73
+
74
+ def threshold_sources(root, laser_id, paths, *, limit_bytes=4 << 20):
75
+ """Newest audited configuration save of each path; explicit when none is recorded."""
76
+ from ldcon.core.agent_authority import approval_root
77
+ note = ('No audited configuration save in the scanned ledger set this value; it may predate the audit '
78
+ 'or have been edited outside the dashboard. The date is unknown, not inferred.')
79
+ result = {path: dict(status='not_recorded', note=note) for path in paths}
80
+ if root is None or not paths:
81
+ return {path: dict(status='unavailable', note='No host ledger root in this context') for path in paths}
82
+ try:
83
+ with (approval_root(root, laser_id)/'audit.jsonl').open('rb') as stream:
84
+ stream.seek(0, 2); size = stream.tell(); stream.seek(max(0, size-limit_bytes))
85
+ raw = stream.read()
86
+ except (OSError, ValueError) as error:
87
+ return {path: dict(status='unavailable', note='Configuration audit unreadable: '+type(error).__name__) for path in paths}
88
+ missing = set(paths)
89
+ for line in reversed(raw.splitlines()):
90
+ if not missing:
91
+ break
92
+ try:
93
+ record = json.loads(line)
94
+ except (UnicodeError, ValueError):
95
+ continue
96
+ if not isinstance(record, dict) or record.get('event') != 'configuration_saved':
97
+ continue
98
+ for row in record.get('changes') or []:
99
+ if isinstance(row, dict) and row.get('path') in missing:
100
+ stamp = record.get('unix_s')
101
+ result[row['path']] = dict(status='recorded', unix_s=stamp,
102
+ utc=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(stamp)) if finite(stamp) else None,
103
+ actor=record.get('actor'), old=row.get('old'), new=row.get('new'),
104
+ authority=record.get('authority'), source='host configuration audit (configuration_saved)')
105
+ missing.discard(row['path'])
106
+ if size > limit_bytes:
107
+ for path in missing:
108
+ result[path]['note'] += ' Only the newest {} bytes of the audit were scanned.'.format(limit_bytes)
109
+ return result
110
+
111
+
112
+ def thresholds(document):
113
+ result = {}
114
+ devices = document.get('devices', {})
115
+ lock_device = document.get('interfaces', {}).get('lock', {}).get('device')
116
+ if lock_device:
117
+ devices = {lock_device:devices.get(lock_device, {})}
118
+ for device, value in devices.items():
119
+ optical = value.get('control_settings', {}).get('optical', {})
120
+ for category in QUALITY_MAPS:
121
+ for metric, limit in optical.get(category, {}).items():
122
+ if metric in QUALITY_METRICS and finite(limit) and limit >= 0:
123
+ result[f'devices.{device}.control_settings.optical.{category}.{metric}'] = limit
124
+ node = value
125
+ for key in DITHER_QUALITY_LEAF:
126
+ node = node.get(key, {}) if isinstance(node, dict) else None
127
+ if finite(node) and node > 0:
128
+ result[f'devices.{device}{DITHER_SUFFIX}'] = node
129
+ return result
130
+
131
+
132
+ class QualityReview:
133
+ def __init__(self, directory, document, *, persist=True, root=None):
134
+ self.directory = directory
135
+ self.persist = persist
136
+ self.root = root
137
+ self.laser_id = document.get('id')
138
+ self.old = thresholds(document)
139
+ self._sources = None
140
+ self.source_revision = digest(document)
141
+ self.measurements = {}
142
+ self.no_write_rows = []
143
+ self.outcome = None
144
+
145
+ def save(self, name, value):
146
+ if self.persist:
147
+ atomic_json(self.directory/name, value)
148
+
149
+ def observe(self, snapshot):
150
+ row = snapshot.get('latest_result') or {}
151
+ if row.get('state') in NO_WRITE_STATES and row.get('id') and no_write(row):
152
+ if row not in self.no_write_rows:
153
+ self.no_write_rows.append(copy.deepcopy(row))
154
+ return
155
+ if row.get('state') not in ('measured', 'adopted') or not row.get('id'):
156
+ return
157
+ record = dict(result=copy.deepcopy(row), controller_job_id=snapshot.get('controller_job_id'),
158
+ segment=snapshot.get('status', {}).get('segment', 0))
159
+ previous = self.measurements.get(row['id'])
160
+ if previous is not None:
161
+ if previous['result'] != record['result'] or previous['controller_job_id'] != record['controller_job_id']:
162
+ raise ValueError('Owner measurement identity changed; quality evidence is inconsistent')
163
+ return
164
+ self.measurements[row['id']] = record
165
+ self.save('adjustment-quality-evidence.json', self.measurements)
166
+
167
+ def context(self):
168
+ from ldcon.jobs.quality_baseline import safe_read
169
+ baseline = safe_read(self.directory.parent.parent/'quality-baseline.json') if self.directory.parent.name == 'jobs' else {}
170
+ return dict(policy_revision=REVISION,
171
+ last_accepted_adjustment_baseline=baseline,
172
+ phase='reviewed_candidate' if self.outcome else 'restore_existing_quality_then_explore',
173
+ supported_action='record_quality_outcome', application='host_save_after_verified_adoption',
174
+ old_thresholds=self.old, source_snapshot_revision=self.source_revision,
175
+ source_scope='task laser.yaml snapshot; not a live configuration CAS revision',
176
+ measurement_ids=list(self.measurements), outcome=self.outcome,
177
+ revisable_thresholds=sorted(self.old),
178
+ revisable_scope='optical single_mode_max scalar leaves and the lock device steady_lock.window.warning_response '
179
+ '(dither quality limit); window.edge_response (window-search boundary) and safety keys are outside this review',
180
+ dither_quality=self.dither_context(),
181
+ required_verification='Two separate owner-accepted post-settle windows at the selected current, in the current segment; adoption measures again.',
182
+ historical_context='adjustment-input-1.json recorded_context and supplied knowledge; cite evidence and coverage, mark missing history explicitly')
183
+
184
+ def dither_context(self):
185
+ """Current dither limit, its recorded source/date and the dither fits measured in this task."""
186
+ paths = [p for p in self.old if is_dither(p)]
187
+ if not paths:
188
+ return dict(status='not_configured', note='No positive steady_lock.window.warning_response on the lock device')
189
+ if self._sources is None:
190
+ self._sources = threshold_sources(self.root, self.laser_id, sorted(self.old)) if self.laser_id else {}
191
+ path = paths[0]; limit = self.old[path]
192
+ rows, unit = [], None
193
+ for identifier, record in self.measurements.items():
194
+ row = record['result']; fit = row.get('dither') or {}
195
+ unit = unit or fit.get('dither_response_unit')
196
+ if fit.get('dither_response_valid') is True and finite(fit.get('dither_response')):
197
+ rows.append(dict(id=identifier, target=row.get('target'), segment=record['segment'],
198
+ value=fit['dither_response'], source_start_unix_s=fit.get('dither_source_start_unix_s'),
199
+ source_end_unix_s=fit.get('dither_source_unix_s')))
200
+ values = sorted(r['value'] for r in rows)
201
+ distribution = dict(count=len(values),
202
+ source='valid post-settle dither fits of owner-accepted results in this task (all points, all segments)')
203
+ if values:
204
+ distribution.update(min=values[0], median=statistics.median(values), max=values[-1],
205
+ above_current_limit=sum(v > limit for v in values), samples=rows[-20:])
206
+ return dict(path=path, current_limit=limit, unit=unit or 'owner dither_response_unit (nm per current-axis unit)',
207
+ source=self._sources.get(path, dict(status='unavailable')), recent_measured=distribution,
208
+ monitor_binding='agent.monitor follow_quality_thresholds dither_response condition and the dashboard quality line read this saved value',
209
+ excluded='window.edge_response is the window-search experiment boundary, not this quality limit',
210
+ telemetry='full series: VictoriaMetrics ldcon_dither_response / ldcon_dither_warning_threshold (kind=owner_sample)')
211
+
212
+ def _verification(self, identifiers, current, snapshot):
213
+ if (not isinstance(identifiers, list) or len(identifiers) < 2 or
214
+ any(not isinstance(i, str) for i in identifiers) or
215
+ len(set(identifiers)) != len(identifiers)):
216
+ raise ValueError('Quality review requires at least two distinct measured result IDs')
217
+ if not finite(current):
218
+ raise ValueError('A finite selected current is required')
219
+ latest = snapshot.get('latest_result') or {}
220
+ if latest.get('id') not in identifiers:
221
+ raise ValueError('Repeat the selected point with current owner evidence before quality review')
222
+ expected = (snapshot.get('controller_job_id'), snapshot.get('status', {}).get('segment', 0))
223
+ windows, contexts, samples, dither_fits, temperatures = [], set(), [], [], []
224
+ for identifier in identifiers:
225
+ record = self.measurements.get(identifier)
226
+ if not record or (record['controller_job_id'], record['segment']) != expected:
227
+ raise ValueError('Quality measurements must belong to the current controller and segment')
228
+ row = record['result']
229
+ if not finite(row.get('target')) or abs(row['target']-current) > 1e-9:
230
+ raise ValueError('Repeat measurements must be at the selected current')
231
+ frames, settled = row.get('optical_frames'), row.get('post_settle_unix_s')
232
+ if not isinstance(frames, list) or len(frames) < 5 or not finite(settled):
233
+ raise ValueError('Complete owner post-settle measurement frames are required')
234
+ times = []
235
+ for frame in frames:
236
+ observation = frame.get('observation') or {}
237
+ timestamp = (observation.get('frequency') or {}).get('acquired_unix_s')
238
+ owner = frame.get('owner_context') or {}
239
+ temperature = (frame.get('outputs') or {}).get('temperature')
240
+ if (not finite(timestamp) or timestamp <= settled or
241
+ frame.get('native_pi_active') is not True or
242
+ not observation.get('mode_id') or not finite(temperature) or
243
+ not all(owner.get(k) for k in ('system_id', 'generation', 'policy_revision'))):
244
+ raise ValueError('Verified owner, branch and post-settle source times are required')
245
+ times.append(timestamp)
246
+ contexts.add(digest([owner, observation['mode_id'], frame.get('target_frequency_hz')]))
247
+ temperatures.append(temperature)
248
+ samples.append(observation.get('metrics') or {})
249
+ if times != sorted(set(times)):
250
+ raise ValueError('Repeated source acquisitions do not verify stability')
251
+ fit = row.get('dither') or {}
252
+ start, end = fit.get('dither_source_start_unix_s'), fit.get('dither_source_unix_s')
253
+ if (fit.get('dither_response_valid') is not True or not finite(fit.get('dither_response'))
254
+ or not finite(start) or not finite(end) or not settled < start <= end):
255
+ raise ValueError('A valid wholly post-settle dither window is required')
256
+ dither_fits.append(dict(id=identifier, value=fit['dither_response'],
257
+ source_start_unix_s=start, source_end_unix_s=end))
258
+ windows.append((min(times[0], start), max(times[-1], end)))
259
+ windows.sort()
260
+ if len(contexts) != 1 or any(a[1] >= b[0] for a, b in zip(windows, windows[1:])):
261
+ raise ValueError('Separate same-context windows are required; overlapping or changed-branch evidence is invalid')
262
+ metrics = {}
263
+ for metric in QUALITY_METRICS:
264
+ values = [sample.get(metric) for sample in samples]
265
+ if all(finite(v) for v in values):
266
+ metrics[metric] = dict(min=min(values), max=max(values))
267
+ return dict(measurement_ids=identifiers, source_windows_unix_s=windows,
268
+ metric_ranges=metrics, dither_range=[min(f['value'] for f in dither_fits), max(f['value'] for f in dither_fits)],
269
+ dither_aggregate=dither_aggregate(dither_fits),
270
+ temperature_range=[min(temperatures), max(temperatures)],
271
+ observation='repeated same-context samples; no long-term stability or physical single-mode guarantee')
272
+
273
+ def record(self, parameters, snapshot):
274
+ if set(parameters) != FIELDS:
275
+ raise ValueError('Quality outcome fields are incomplete or unsupported')
276
+ classification = parameters['classification']
277
+ if classification not in ('adequate', 'restored_adequate', 'best_practical'):
278
+ raise ValueError('Quality classification must preserve an unmet historical criterion')
279
+ margin = parameters['margin_fraction']
280
+ if not finite(margin) or not 0 < margin < 1:
281
+ raise ValueError('Specify a positive practical margin fraction below one')
282
+ for key in ('rationale', 'follow_up'):
283
+ if not isinstance(parameters[key], str) or not parameters[key].strip():
284
+ raise ValueError('Quality rationale and follow-up are required')
285
+ history = parameters['history_assessment']
286
+ if not isinstance(history, dict) or set(history) != HISTORY_FIELDS:
287
+ raise ValueError('History interval, coverage, comparability and practical requirement are required')
288
+ refs, interval = history['evidence_refs'], history['interval_unix_s']
289
+ if (not isinstance(refs, list) or not refs or any(not isinstance(r, str) or not r.strip() for r in refs)
290
+ or not isinstance(interval, list) or len(interval) != 2
291
+ or not all(finite(t) for t in interval) or interval[0] >= interval[1]
292
+ or any(not isinstance(history[k], str) or not history[k].strip()
293
+ for k in HISTORY_FIELDS-{'evidence_refs', 'interval_unix_s'})):
294
+ raise ValueError('History evidence and its limitations must be explicit')
295
+ changes = parameters['changes']
296
+ if (not isinstance(changes, dict) or len(changes) > 8 or
297
+ any(k not in self.old or not finite(v) or v < 0 or (is_dither(k) and v <= 0)
298
+ for k, v in changes.items())):
299
+ raise ValueError('Only existing recognized optical classification or dither quality scalar candidates are supported')
300
+ verification = self._verification(parameters['measurement_ids'], parameters['selected_current'], snapshot)
301
+ new = {**self.old, **changes}
302
+ comparisons = {}
303
+ for path, limit in self.old.items():
304
+ if is_dither(path):
305
+ # The dither limit is compared with the mean of the valid fits
306
+ # (max and std are reported, not compared).
307
+ aggregate = verification['dither_aggregate']
308
+ observed = aggregate['mean']
309
+ comparisons[path] = dict(observed_max=aggregate['max'], observed_mean=observed,
310
+ aggregation=DITHER_AGGREGATION, rule=DITHER_RULE, n=aggregate['n'],
311
+ mean=aggregate['mean'], max=aggregate['max'], std=aggregate['std'])
312
+ else:
313
+ values = verification['metric_ranges'].get(path.rsplit('.', 1)[-1])
314
+ observed = values['max'] if values else None
315
+ comparisons[path] = dict(observed_max=observed, aggregation='max_of_frames')
316
+ comparisons[path].update(old_limit=limit, proposed_limit=new[path],
317
+ meets_old=None if observed is None else observed <= limit,
318
+ meets_old_with_margin=None if observed is None else observed <= limit*(1-margin),
319
+ meets_proposed=None if observed is None else observed <= new[path])
320
+ primary = [row for path, row in comparisons.items() if '.single_mode_max.' in path]
321
+ adequate = bool(primary) and all(row['meets_old_with_margin'] is True for row in primary)
322
+ # A changed threshold whose old value is unmet is a reclassification,
323
+ # never a restored or physically improved quality.
324
+ reclassified = sorted(p for p in changes if comparisons[p]['meets_old_with_margin'] is not True)
325
+ if classification in ('adequate', 'restored_adequate') and (not adequate or reclassified):
326
+ raise ValueError('Existing quality with margin is unverified or unmet; do not relabel degradation as restored')
327
+ result = dict(schema_version=1, policy_revision=REVISION,
328
+ source_snapshot_revision=self.source_revision, old_criterion_revision=digest(self.old),
329
+ proposed_criterion_revision=digest(new), old_thresholds=self.old, proposed_thresholds=new,
330
+ changes=changes, classification=classification, selected_current=parameters['selected_current'],
331
+ historical_quality_with_margin=adequate, comparisons=comparisons, verification=verification,
332
+ reclassified_paths=reclassified, physical_improvement_claimed=False,
333
+ reclassification_note='A revised limit changes the operating criterion only; old values and raw evidence are kept.',
334
+ margin_fraction=margin, history_assessment=copy.deepcopy(history),
335
+ history_validation='agent assessment; cited external history has not been independently verified by this host',
336
+ rationale=parameters['rationale'], follow_up=parameters['follow_up'],
337
+ decision_revision=snapshot.get('status', {}).get('decision_revision'),
338
+ no_write_rows_at_record=len(self.no_write_rows),
339
+ application_status='pending_point_adoption' if changes else 'thresholds_unchanged',
340
+ applied=False, physical_single_mode_guarantee=False,
341
+ pending_reason='Host applies requested changes only after verified point adoption and fresh scoped CAS/admission checks; saved/queued is not owner-effective application. Unknown writes are not retried.')
342
+ self.outcome = result
343
+ self.save('adjustment-quality-outcome.json', result)
344
+ return result
345
+
346
+ def require_adoption(self, value, snapshot):
347
+ """The recorded outcome must still describe the owner evidence being adopted.
348
+
349
+ The owner advances ``decision_revision`` as ``digest([previous, row])`` for
350
+ every command result row (``AdaptiveWindowSession._result``) and by other
351
+ digests for revocation, interruption, termination and replan. An outcome
352
+ stays current only if the present revision is reproduced exactly from the
353
+ recorded one by chaining observed no-write receipts (``NO_WRITE_STATES``),
354
+ so any measured/adopted/other row or session event in between refuses.
355
+ """
356
+ if not self.outcome or self.outcome['selected_current'] != value:
357
+ raise ValueError('Record a current quality outcome after repeated measurements before adoption')
358
+ current = snapshot.get('status', {}).get('decision_revision')
359
+ revision = self.outcome['decision_revision']
360
+ if revision == current:
361
+ return
362
+ chained = []
363
+ for row in self.no_write_rows[self.outcome.get('no_write_rows_at_record', 0):]:
364
+ revision = digest([revision, row])
365
+ chained.append(row)
366
+ if isinstance(current, str) and revision == current:
367
+ self.outcome['revision_advanced_by_no_write'] = [dict(id=r['id'], state=r['state'],
368
+ action=(r.get('command') or {}).get('action'), reason=r.get('reason')) for r in chained]
369
+ self.save('adjustment-quality-outcome.json', self.outcome)
370
+ return
371
+ raise ValueError('Owner evidence changed after the quality outcome (a new measured, adopted or '
372
+ 'session result, not only a no-write rejection); record a current quality outcome '
373
+ 'after repeated measurements before adoption')
374
+
375
+ def finalize(self, snapshot):
376
+ """Keep physical adoption and the final point's observed quality separate."""
377
+ row = snapshot.get('latest_result') or {}
378
+ if not self.outcome or row.get('state') != 'adopted':
379
+ return
380
+ self.outcome['adoption_result_id'] = row.get('id')
381
+ try:
382
+ verification = self._verification(
383
+ self.outcome['verification']['measurement_ids']+[row['id']],
384
+ self.outcome['selected_current'], snapshot)
385
+ primary = [(path, limit) for path, limit in self.old.items() if '.single_mode_max.' in path]
386
+ ranges = verification['metric_ranges']
387
+ self.outcome['post_adoption_meets_old_with_margin'] = bool(primary) and all(
388
+ path.rsplit('.', 1)[-1] in ranges and
389
+ ranges[path.rsplit('.', 1)[-1]]['max'] <= limit*(1-self.outcome['margin_fraction'])
390
+ for path, limit in primary)
391
+ dither = [limit for path, limit in self.old.items() if is_dither(path)]
392
+ if dither:
393
+ self.outcome['post_adoption_dither_meets_old_with_margin'] = dither_meets(
394
+ verification['dither_aggregate'], dither[0], self.outcome['margin_fraction'])
395
+ self.outcome['post_adoption_verification'] = verification
396
+ except (ValueError, KeyError) as error:
397
+ self.outcome['post_adoption_meets_old_with_margin'] = None
398
+ self.outcome['post_adoption_verification'] = dict(status='unverified', reason=str(error))
399
+ self.save('adjustment-quality-outcome.json', self.outcome)