xrefkit 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. {xrefkit-0.5.0/xrefkit.egg-info → xrefkit-0.6.0}/PKG-INFO +39 -1
  2. {xrefkit-0.5.0 → xrefkit-0.6.0}/README.md +38 -0
  3. {xrefkit-0.5.0 → xrefkit-0.6.0}/pyproject.toml +1 -1
  4. xrefkit-0.6.0/tests/test_attention_pet.py +257 -0
  5. xrefkit-0.6.0/tests/test_attention_pet_client_protocol.py +302 -0
  6. xrefkit-0.6.0/tests/test_attention_pet_codex_session.py +100 -0
  7. xrefkit-0.6.0/tests/test_attention_pet_fit.py +396 -0
  8. xrefkit-0.6.0/tests/test_embedded_startup_pack.py +45 -0
  9. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/__init__.py +1 -1
  10. xrefkit-0.6.0/xrefkit/attention_pet/__init__.py +1 -0
  11. xrefkit-0.6.0/xrefkit/attention_pet/__main__.py +78 -0
  12. xrefkit-0.6.0/xrefkit/attention_pet/client.py +80 -0
  13. xrefkit-0.6.0/xrefkit/attention_pet/client_protocol.py +141 -0
  14. xrefkit-0.6.0/xrefkit/attention_pet/codex_session.py +151 -0
  15. xrefkit-0.6.0/xrefkit/attention_pet/evaluator.py +232 -0
  16. xrefkit-0.6.0/xrefkit/attention_pet/model.py +246 -0
  17. xrefkit-0.6.0/xrefkit/attention_pet/presentation.py +195 -0
  18. xrefkit-0.6.0/xrefkit/attention_pet/profiles.py +51 -0
  19. xrefkit-0.6.0/xrefkit/attention_pet/scenarios.py +42 -0
  20. xrefkit-0.6.0/xrefkit/attention_pet/server.py +165 -0
  21. xrefkit-0.6.0/xrefkit/attention_pet/store.py +118 -0
  22. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/cli.py +5 -0
  23. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/catalog.py +15 -1
  24. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/startup_contract_pack.py +8 -6
  25. xrefkit-0.6.0/xrefkit/resources/attention_pet/client-state.schema.json +306 -0
  26. xrefkit-0.6.0/xrefkit/resources/attention_pet/pet.css +97 -0
  27. xrefkit-0.6.0/xrefkit/resources/attention_pet/pet.html +42 -0
  28. xrefkit-0.6.0/xrefkit/resources/attention_pet/pet.js +249 -0
  29. {xrefkit-0.5.0 → xrefkit-0.6.0/xrefkit.egg-info}/PKG-INFO +39 -1
  30. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/SOURCES.txt +21 -0
  31. {xrefkit-0.5.0 → xrefkit-0.6.0}/LICENSE +0 -0
  32. {xrefkit-0.5.0 → xrefkit-0.6.0}/setup.cfg +0 -0
  33. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_artifact_selected_family.py +0 -0
  34. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_boundary_analysis.py +0 -0
  35. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_brownfield_csharp_structure_definitions.py +0 -0
  36. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_brownfield_file_editing_protocol.py +0 -0
  37. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_business_intake_conversation_definitions.py +0 -0
  38. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_business_intake_definition.py +0 -0
  39. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_calibration_lint.py +0 -0
  40. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_catalog_preparation_definition.py +0 -0
  41. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_check_skill_knowledge_xids.py +0 -0
  42. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_cli.py +0 -0
  43. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_code_constraint_definition.py +0 -0
  44. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_collect_analyzer_sarif.py +0 -0
  45. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_constraint_derivation_family_definition.py +0 -0
  46. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_constraint_derivation_selected_family.py +0 -0
  47. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_contribution_returns.py +0 -0
  48. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_convert_to_xrefkit_skill.py +0 -0
  49. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_cs_scope_probe.py +0 -0
  50. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_csharp_commonality.py +0 -0
  51. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_csharp_naming_profile.py +0 -0
  52. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_ctx.py +0 -0
  53. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_cutover_readiness.py +0 -0
  54. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_dashboard.py +0 -0
  55. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_database_batch_definition_migration.py +0 -0
  56. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_decision_trace.py +0 -0
  57. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_design_business_intake_selected_family.py +0 -0
  58. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_design_planning_definition_migration.py +0 -0
  59. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_dotnet_definition_candidate.py +0 -0
  60. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_editorial_family_definition.py +0 -0
  61. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_editorial_intake_definition.py +0 -0
  62. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_editorial_selected_family.py +0 -0
  63. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_error_policy_audit.py +0 -0
  64. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_error_policy_locator.py +0 -0
  65. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_execution_binding.py +0 -0
  66. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_existing_skill_definition_batch.py +0 -0
  67. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_final_skilldefinition_batch.py +0 -0
  68. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_flow_selected_family.py +0 -0
  69. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_fm_multiroot.py +0 -0
  70. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gate.py +0 -0
  71. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway.py +0 -0
  72. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway_mcp.py +0 -0
  73. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway_skill_adapter.py +0 -0
  74. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway_work_items.py +0 -0
  75. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_goal_desired_state.py +0 -0
  76. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_host_precheck.py +0 -0
  77. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_human_evaluation.py +0 -0
  78. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_implementation_flow_definition.py +0 -0
  79. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_implementation_review_selected_family.py +0 -0
  80. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_inbound_uploads.py +0 -0
  81. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_instruction_workflow.py +0 -0
  82. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_knowledge_relations_validator.py +0 -0
  83. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_language_review_definition.py +0 -0
  84. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_package_routing.py +0 -0
  85. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_setup.py +0 -0
  86. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_subagent_startup.py +0 -0
  87. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_subagent_startup_integration.py +0 -0
  88. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_os_family_definition.py +0 -0
  89. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_os_knowledge_family_definition.py +0 -0
  90. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_os_selected_family.py +0 -0
  91. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_ownership.py +0 -0
  92. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_packmeta.py +0 -0
  93. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_project_quality_baseline.py +0 -0
  94. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_qa_report_definition.py +0 -0
  95. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_resource_provider.py +0 -0
  96. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_runtime_contracts.py +0 -0
  97. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_sarif_to_locator.py +0 -0
  98. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_security_definition_migration.py +0 -0
  99. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition.py +0 -0
  100. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition_governance.py +0 -0
  101. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition_mcp.py +0 -0
  102. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition_runtime.py +0 -0
  103. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_edits.py +0 -0
  104. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_maturity_return.py +0 -0
  105. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_runtime_audit.py +0 -0
  106. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skillmeta.py +0 -0
  107. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skills_sync.py +0 -0
  108. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_structure_catalog.py +0 -0
  109. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_subagent_startup.py +0 -0
  110. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_subagent_startup_boundaries.py +0 -0
  111. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_wbs.py +0 -0
  112. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_workflow_family_definition.py +0 -0
  113. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xref.py +0 -0
  114. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_instance.py +0 -0
  115. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_tools.py +0 -0
  116. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_v2_discovery.py +0 -0
  117. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_v2_models.py +0 -0
  118. {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_v2_pipeline.py +0 -0
  119. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/__main__.py +0 -0
  120. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/boundary_analysis.py +0 -0
  121. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/catalog_cli.py +0 -0
  122. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/contracts.py +0 -0
  123. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/ctx.py +0 -0
  124. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/dashboard.py +0 -0
  125. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/decision_trace.py +0 -0
  126. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/discovery.py +0 -0
  127. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/execution_binding.py +0 -0
  128. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/execution_binding_cli.py +0 -0
  129. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/gate.py +0 -0
  130. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/gateway.py +0 -0
  131. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/goalstate.py +0 -0
  132. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/hashing.py +0 -0
  133. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/host_precheck.py +0 -0
  134. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/import_skill.py +0 -0
  135. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/instance.py +0 -0
  136. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/loaders.py +0 -0
  137. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/__init__.py +0 -0
  138. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/audit.py +0 -0
  139. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/bootstrap.py +0 -0
  140. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/cli.py +0 -0
  141. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/client_cache.py +0 -0
  142. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/client_flow.py +0 -0
  143. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/context_registry.py +0 -0
  144. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/context_token.py +0 -0
  145. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/contracts.py +0 -0
  146. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/contribution_adoption.py +0 -0
  147. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/contribution_returns.py +0 -0
  148. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/dist.py +0 -0
  149. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/gateway.py +0 -0
  150. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/inbound_uploads.py +0 -0
  151. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/knowledge_edits.py +0 -0
  152. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/ownership.py +0 -0
  153. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/repository.py +0 -0
  154. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/schemas.py +0 -0
  155. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/server.py +0 -0
  156. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/setup.py +0 -0
  157. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/skill_edits.py +0 -0
  158. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/skill_maturity.py +0 -0
  159. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/subagent_startup.py +0 -0
  160. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp_tools.py +0 -0
  161. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/__init__.py +0 -0
  162. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/common.py +0 -0
  163. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/effective_bundle.py +0 -0
  164. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/human_evaluation.py +0 -0
  165. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/local_manifest.py +0 -0
  166. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/package_manifest.py +0 -0
  167. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/run_log.py +0 -0
  168. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/server_config.py +0 -0
  169. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/skill_definition.py +0 -0
  170. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/operations_cli.py +0 -0
  171. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/ownership.py +0 -0
  172. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/packmeta.py +0 -0
  173. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/registry.py +0 -0
  174. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resolver.py +0 -0
  175. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resource_provider.py +0 -0
  176. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/contracts.json +0 -0
  177. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/current.json +0 -0
  178. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
  179. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
  180. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
  181. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
  182. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/model_body.md +0 -0
  183. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/000_agent_entry.md +0 -0
  184. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/011_startup_xref_routing.md +0 -0
  185. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/015_shared_memory_operations.md +0 -0
  186. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/016_uncertainty_protocol.md +0 -0
  187. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/017_base_and_xref_layering.md +0 -0
  188. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/053_context_direction_security_guard.md +0 -0
  189. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/runlog.py +0 -0
  190. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skill_definition.py +0 -0
  191. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skill_definition_catalog.py +0 -0
  192. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skill_definition_governance.py +0 -0
  193. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skillmeta.py +0 -0
  194. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skillrun.py +0 -0
  195. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skills_sync.py +0 -0
  196. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/structure_catalog.py +0 -0
  197. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/subagent_startup.py +0 -0
  198. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/tools/__init__.py +0 -0
  199. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/tools/__main__.py +0 -0
  200. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/v2_cli.py +0 -0
  201. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/wbs.py +0 -0
  202. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/workspace.py +0 -0
  203. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/xref.py +0 -0
  204. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/dependency_links.txt +0 -0
  205. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/entry_points.txt +0 -0
  206. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/requires.txt +0 -0
  207. {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrefkit
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -47,6 +47,44 @@ record evidence, preserve human judgment, and apply explicit completion checks.
47
47
 
48
48
  ## Why XRefKit?
49
49
 
50
+ Experimental local tool: [Attention Pet](projects/attention-pet/README.en.md) helps
51
+ people assess capability sufficiency and lower inference-cost candidates separately.
52
+ Its Model Fit / Cost Fit estimates use uncalibrated profiles; it does not measure internal
53
+ attention or switch models automatically.
54
+
55
+ ### Codex-only preview: Attention Pet
56
+
57
+ Attention Pet is a small local companion for Codex work. It reads the current
58
+ Codex chat's local session record, estimates whether the selected model meets
59
+ the visible work requirements, and shows the result through a compact animated
60
+ pet. It also shows lower inference-cost comparison candidates when the
61
+ experimental profiles support one. The estimate is observational and does not
62
+ change the Codex model, reasoning level, or chat.
63
+
64
+ It does not measure token usage. It estimates the structural complexity of the
65
+ work context the AI must handle, including retained items, dependencies,
66
+ constraints, decision depth, conflicts, and dispersed evidence. It does not
67
+ read model-internal attention or remaining context-window capacity.
68
+
69
+ Run it from a Codex terminal in this repository so `CODEX_THREAD_ID` identifies
70
+ the current chat:
71
+
72
+ ```powershell
73
+ python -m xrefkit.attention_pet serve --port 8769
74
+ ```
75
+
76
+ Open the printed `http://127.0.0.1:8769/` address in the Codex browser panel.
77
+ The display follows the browser's preferred language: Japanese is used for a
78
+ Japanese preference, with English as the fallback. Stop the process with
79
+ Ctrl+C.
80
+
81
+ This Codex preview is bound to the chat that launched it. Selecting another
82
+ chat does not move the Pet automatically; launch it again from that chat when
83
+ you want a separate view. The model and reasoning dropdowns only compare
84
+ estimates and do not change the active Codex settings. See the
85
+ [Attention Pet guide](projects/attention-pet/README.en.md) for interpretation,
86
+ limitations, client integration, and API details.
87
+
50
88
  Using AI for real work creates recurring operating problems:
51
89
 
52
90
  ![Why XRefKit is needed](human-docs/en/assets/why_xrefkit_needed/whatis_xrefkit.png)
@@ -8,6 +8,44 @@ record evidence, preserve human judgment, and apply explicit completion checks.
8
8
 
9
9
  ## Why XRefKit?
10
10
 
11
+ Experimental local tool: [Attention Pet](projects/attention-pet/README.en.md) helps
12
+ people assess capability sufficiency and lower inference-cost candidates separately.
13
+ Its Model Fit / Cost Fit estimates use uncalibrated profiles; it does not measure internal
14
+ attention or switch models automatically.
15
+
16
+ ### Codex-only preview: Attention Pet
17
+
18
+ Attention Pet is a small local companion for Codex work. It reads the current
19
+ Codex chat's local session record, estimates whether the selected model meets
20
+ the visible work requirements, and shows the result through a compact animated
21
+ pet. It also shows lower inference-cost comparison candidates when the
22
+ experimental profiles support one. The estimate is observational and does not
23
+ change the Codex model, reasoning level, or chat.
24
+
25
+ It does not measure token usage. It estimates the structural complexity of the
26
+ work context the AI must handle, including retained items, dependencies,
27
+ constraints, decision depth, conflicts, and dispersed evidence. It does not
28
+ read model-internal attention or remaining context-window capacity.
29
+
30
+ Run it from a Codex terminal in this repository so `CODEX_THREAD_ID` identifies
31
+ the current chat:
32
+
33
+ ```powershell
34
+ python -m xrefkit.attention_pet serve --port 8769
35
+ ```
36
+
37
+ Open the printed `http://127.0.0.1:8769/` address in the Codex browser panel.
38
+ The display follows the browser's preferred language: Japanese is used for a
39
+ Japanese preference, with English as the fallback. Stop the process with
40
+ Ctrl+C.
41
+
42
+ This Codex preview is bound to the chat that launched it. Selecting another
43
+ chat does not move the Pet automatically; launch it again from that chat when
44
+ you want a separate view. The model and reasoning dropdowns only compare
45
+ estimates and do not change the active Codex settings. See the
46
+ [Attention Pet guide](projects/attention-pet/README.en.md) for interpretation,
47
+ limitations, client integration, and API details.
48
+
11
49
  Using AI for real work creates recurring operating problems:
12
50
 
13
51
  ![Why XRefKit is needed](human-docs/en/assets/why_xrefkit_needed/whatis_xrefkit.png)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xrefkit"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,257 @@
1
+ import json
2
+ import threading
3
+ from urllib.error import HTTPError
4
+ from urllib.request import Request, urlopen
5
+
6
+ import pytest
7
+ from pydantic import ValidationError
8
+
9
+ from xrefkit.attention_pet.evaluator import Weights, evaluate
10
+ from xrefkit.attention_pet.model import Conversation, Edge, Item, WorkingSet, extract
11
+ from xrefkit.attention_pet.scenarios import scenario
12
+ from xrefkit.attention_pet.server import make_server
13
+ from xrefkit.attention_pet.store import Store
14
+
15
+
16
+ def fixture(name="A", step=1):
17
+ return WorkingSet.model_validate(scenario(name, step)["workingSet"])
18
+
19
+
20
+ def test_scenario_a_independent_requests_grow_gradually():
21
+ scores = [evaluate(fixture("A", s))["ral"] for s in range(1, 6)]
22
+ assert scores == sorted(scores)
23
+ assert max(b - a for a, b in zip(scores, scores[1:])) < 10
24
+
25
+
26
+ def test_scenario_b_relationships_cost_more_than_independent_items():
27
+ a, b = evaluate(fixture("A", 5)), evaluate(fixture("B", 5))
28
+ assert a["features"]["active_items"] == b["features"]["active_items"]
29
+ assert b["ral"] > a["ral"] + 20
30
+ assert b["features"]["dependency_edges"] > 0
31
+
32
+
33
+ def test_scenario_c_low_load_can_have_wrong_trajectory():
34
+ states = [evaluate(fixture("C", s)) for s in range(1, 4)]
35
+ assert len({s["ral"] for s in states}) == 1
36
+ assert states[-1]["ral"] < 40
37
+ assert states[-1]["trajectoryStability"] < states[0]["trajectoryStability"]
38
+ assert states[-1]["state"] == "WRONG TRAJECTORY"
39
+ assert states[-1]["expression"] == "wrong_trajectory"
40
+ assert "restart" in states[-1]["recommendedActions"]
41
+
42
+
43
+ def test_scenario_d_summary_lowers_load_without_erasing_constraints():
44
+ store = Store()
45
+ before = store.submit(fixture("D"))
46
+ result = store.recover("summarize", before["state"]["observedAt"])
47
+ assert before["state"]["ral"] >= 75
48
+ assert result["state"]["ral"] < before["state"]["ral"]
49
+ assert result["state"]["deltaRal"] < 0
50
+ assert result["state"]["features"]["constraint"] == before["state"]["features"]["constraint"]
51
+ assert result["state"]["features"]["dependency_edges"] == before["state"]["features"]["dependency_edges"]
52
+ assert len(result["workingSet"]["items"]) == len(before["workingSet"]["items"])
53
+
54
+
55
+ def test_scenario_e_restart_requires_fresh_validation_to_recover():
56
+ store = Store()
57
+ before = store.submit(fixture("E", 3))
58
+ handoff = store.recover("restart", before["state"]["observedAt"])
59
+ assert handoff["state"] == before["state"] # Download is not a context restart.
60
+ fresh = fixture("E", 4)
61
+ fresh.observations = [o for o in fresh.observations if o.context != fresh.contextId]
62
+ unknown = store.submit(fresh)
63
+ assert unknown["state"]["trajectoryStability"] is None
64
+ recovered = store.submit(fixture("E", 5))
65
+ assert recovered["state"]["trajectoryStability"] > before["state"]["trajectoryStability"]
66
+ assert recovered["state"]["ral"] == before["state"]["ral"]
67
+
68
+
69
+ def test_trend_uses_time_window_and_reports_feature_deltas():
70
+ store = Store()
71
+ a = store.submit(fixture("B", 1))
72
+ b = store.submit(fixture("B", 4))
73
+ assert b["state"]["deltaRal"] == b["state"]["ral"] - a["state"]["ral"]
74
+ assert b["state"]["trend"] == "rapid_rise"
75
+ assert {c["feature"] for c in b["state"]["changes"]} >= {"constraint", "dependency_edges"}
76
+ ws = fixture("B", 5)
77
+ ws.observedAt = 2000.0
78
+ assert store.submit(ws)["state"]["trend"] != "rapid_rise"
79
+
80
+
81
+ def test_unknown_trajectory_does_not_override_load_and_task_switch_does_not_compare():
82
+ ws = fixture()
83
+ ws.observations = []
84
+ assert evaluate(ws)["state"] == "UNKNOWN"
85
+ assert evaluate(ws)["expression"] == "stable"
86
+ store = Store()
87
+ store.submit(ws)
88
+ assert store.submit(fixture("B"))["state"]["deltaRal"] is None
89
+
90
+
91
+ @pytest.mark.parametrize("coverage", ["partial", "reviewed"])
92
+ @pytest.mark.parametrize("step,expression", [(1,"stable"),(3,"loaded"),(5,"strained")])
93
+ def test_structural_load_remains_visible_without_trajectory(coverage, step, expression):
94
+ ws = fixture("B", step)
95
+ ws.coverage = coverage
96
+ ws.observations = []
97
+ state = evaluate(ws)
98
+ assert state["trajectoryStability"] is None
99
+ assert state["state"] == "UNKNOWN"
100
+ assert state["expression"] == expression
101
+
102
+
103
+ def test_fix_retains_relations_and_does_not_fake_lower_load():
104
+ store = Store()
105
+ before = store.submit(fixture("B", 3))
106
+ fixed = store.recover("fix", before["state"]["observedAt"])
107
+ assert fixed["state"]["ral"] == before["state"]["ral"]
108
+ assert all(i["fixed"] for i in fixed["workingSet"]["items"] if i["kind"] == "decision")
109
+
110
+
111
+ def test_summary_preserves_history_referenced_by_active_items():
112
+ ws = fixture("D")
113
+ ws.dependencies.append(Edge(source="d0", target="h0-0", evidence="needed evidence"))
114
+ store = Store()
115
+ store.submit(ws)
116
+ result = store.recover("summarize", ws.observedAt)
117
+ assert next(i for i in result["workingSet"]["items"] if i["id"] == "h0-0")["status"] == "active"
118
+
119
+
120
+ @pytest.mark.parametrize("action", ["split", "externalize", "resolve", "rebase", "restart"])
121
+ def test_handoff_actions_do_not_claim_to_change_live_context(action):
122
+ store = Store()
123
+ before = store.submit(fixture())
124
+ result = store.recover(action, before["state"]["observedAt"])
125
+ assert result["recovery"]["effect"] == "handoff_created"
126
+ assert result["recovery"]["handoff"]["workingSet"] == before["workingSet"]
127
+ assert result["state"] == before["state"]
128
+
129
+
130
+ @pytest.mark.parametrize("mutation", [
131
+ lambda d: d.update(unexpected="field"),
132
+ lambda d: d.update(observedAt=float("nan")),
133
+ lambda d: d.update(items=d["items"] * 2),
134
+ lambda d: d.update(dependencies=[{"source":"absent","target":"d0","evidence":"bad"}]),
135
+ lambda d: d["items"][0].update(depth=-1),
136
+ lambda d: d["items"][0].update(fixed="true"),
137
+ ])
138
+ def test_invalid_boundary_data_is_rejected(mutation):
139
+ data = fixture().model_dump()
140
+ mutation(data)
141
+ with pytest.raises(ValidationError):
142
+ WorkingSet.model_validate(data)
143
+
144
+
145
+ def test_timestamp_replay_and_stale_recovery():
146
+ store = Store()
147
+ ws = fixture()
148
+ first = store.submit(ws)
149
+ assert store.submit(ws) == first
150
+ stale = ws.model_copy(deep=True)
151
+ stale.observedAt = 1.0
152
+ with pytest.raises(ValueError, match="timestamps"):
153
+ store.submit(stale)
154
+ with pytest.raises(ValueError, match="refresh"):
155
+ store.recover("fix", 1.0)
156
+
157
+
158
+ def test_extract_upserts_status_and_marks_unannotated_turn_partial():
159
+ data = scenario("A")["conversation"]
160
+ updated = dict(data["turns"][0]["items"][0], status="done")
161
+ data["turns"].append({"id":"update", "text":"Requirement complete", "items":[updated]})
162
+ ws = extract(Conversation.model_validate(data))
163
+ assert len(ws.items) == 5 and ws.items[0].status == "done"
164
+ data["turns"].append({"id":"unannotated", "text":"A new unknown requirement"})
165
+ assert extract(Conversation.model_validate(data)).coverage == "partial"
166
+ data["turns"][0]["items"][0]["source"] = "missing"
167
+ with pytest.raises(ValueError, match="supplied turn"):
168
+ extract(Conversation.model_validate(data))
169
+
170
+
171
+ def test_persistence_restart_and_failed_write_do_not_mutate_state(tmp_path, monkeypatch):
172
+ from pathlib import Path
173
+ path = tmp_path / "session.json"
174
+ store = Store(path)
175
+ store.submit(fixture("B", 1))
176
+ before = store.submit(fixture("B", 2))
177
+ assert Store(path).view() == before
178
+ def fail(*args):
179
+ raise OSError("disk full")
180
+ monkeypatch.setattr(Path, "replace", fail)
181
+ with pytest.raises(OSError):
182
+ store.submit(fixture("B", 3))
183
+ assert store.view() == before
184
+
185
+
186
+ def test_weights_validation():
187
+ with pytest.raises(ValueError):
188
+ Weights(working_set=-1)
189
+ with pytest.raises(ValueError):
190
+ Weights(working_set=float("nan"))
191
+
192
+
193
+ def test_high_load_alone_never_recommends_restart():
194
+ state = evaluate(fixture("D"))
195
+ assert state["ral"] >= 75 and state["state"] == "HIGH LOAD"
196
+ assert "restart" not in state["recommendedActions"]
197
+
198
+
199
+ @pytest.mark.parametrize("count,band", [(1,"stable"),(16,"loaded"),(24,"strained"),(30,"high_pressure"),(36,"unstable")])
200
+ def test_five_visual_bands_and_critical_override(count, band):
201
+ ws = fixture()
202
+ ws.items = [Item(id=f"g-{i}", kind="goal", text="goal", source="test") for i in range(count)]
203
+ weights = Weights(working_set=100, dependency_complexity=0, constraint_density=0,
204
+ decision_depth=0, conflict_pressure=0, context_dispersion=0)
205
+ assert evaluate(ws, weights=weights)["expression"] == band
206
+ ws.observations = fixture("C", 3).observations
207
+ result = evaluate(ws, weights=weights)
208
+ assert result["state"] == ("CRITICAL" if count >= 30 else "WRONG TRAJECTORY")
209
+
210
+
211
+ def test_empty_partial_snapshot_stays_unknown_and_bounds_hold():
212
+ ws = WorkingSet(taskId="empty",contextId="c",observedAt=1.0,items=[])
213
+ result = evaluate(ws)
214
+ assert result["ral"] == 0 and result["trajectoryStability"] is None
215
+ assert result["state"] == "UNKNOWN"
216
+
217
+
218
+ def test_http_api_auth_origin_validation_recovery_and_assets():
219
+ server, launch = make_server(Store())
220
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
221
+ thread.start()
222
+ base, token = launch.rstrip("/"), server.write_token
223
+ def request(path, data=None, auth=True, origin=None):
224
+ headers = {"Authorization":f"Bearer {token}"} if auth else {}
225
+ if origin:
226
+ headers["Origin"] = origin
227
+ if data is not None:
228
+ headers["Content-Type"] = "application/json"
229
+ req = Request(base + path, json.dumps(data).encode() if data is not None else None, headers)
230
+ with urlopen(req, timeout=5) as response:
231
+ return response.read()
232
+ try:
233
+ assert b"Attention Pet" in request("/", auth=False)
234
+ for path in ("/pet.js", "/pet.css"):
235
+ assert request(path, auth=False)
236
+ assert json.loads(request("/api/state", auth=False))["state"] is None
237
+ with pytest.raises(HTTPError) as error:
238
+ request("/api/snapshot", fixture().model_dump(), auth=False)
239
+ assert error.value.code == 401
240
+ with pytest.raises(HTTPError) as error:
241
+ request("/api/snapshot", fixture().model_dump(), origin="https://example.com")
242
+ assert error.value.code == 403
243
+ with pytest.raises(HTTPError) as error:
244
+ request("/api/snapshot", {"ral":99})
245
+ assert error.value.code == 400
246
+ before = json.loads(request("/api/conversation", scenario("D")["conversation"]))
247
+ after = json.loads(request("/api/recover", {"action":"summarize", "expectedObservedAt":before["state"]["observedAt"]}))
248
+ assert after["state"]["ral"] < before["state"]["ral"]
249
+ assert json.loads(request("/api/state"))["state"] == after["state"]
250
+ with pytest.raises(HTTPError) as error:
251
+ request("/api/demo", {"scenario":"C", "step":3})
252
+ assert error.value.code == 404
253
+ assert json.loads(request("/api/state"))["state"] == after["state"]
254
+ finally:
255
+ server.shutdown()
256
+ server.server_close()
257
+ thread.join(timeout=5)
@@ -0,0 +1,302 @@
1
+ """Same-host client protocol and session switching."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import threading
7
+ from pathlib import Path
8
+ from urllib.error import HTTPError
9
+ from urllib.request import Request, urlopen
10
+
11
+ import pytest
12
+
13
+ from xrefkit.attention_pet.client_protocol import ClientStateSource, PROTOCOL_VERSION
14
+ from xrefkit.attention_pet.client_protocol import ClientState
15
+ from xrefkit.attention_pet.client import AttentionPetClient, ClientProtocolError
16
+ from xrefkit.attention_pet.evaluator import Weights
17
+ from xrefkit.attention_pet.server import make_server
18
+ from xrefkit.attention_pet.store import Store
19
+
20
+
21
+ def payload(session: str, revision: int, item_count: int = 1) -> dict:
22
+ observed = float(100 + revision)
23
+ return {
24
+ "protocolVersion": PROTOCOL_VERSION,
25
+ "provider": "codex-desktop",
26
+ "clientInstanceId": "window-1",
27
+ "sessionId": session,
28
+ "activationRevision": revision,
29
+ "observedAt": observed,
30
+ "model": "gpt-6-sol",
31
+ "reasoning": "medium",
32
+ "capabilities": {
33
+ "activeSessionChanged": True,
34
+ "promptSubmitted": True,
35
+ "modelSelected": True,
36
+ "fullConversationAccess": False,
37
+ },
38
+ "workingSet": {
39
+ "schemaVersion": 1,
40
+ "taskId": "task-" + session,
41
+ "contextId": session,
42
+ "observedAt": observed,
43
+ "coverage": "partial",
44
+ "items": [
45
+ {"id": f"item-{index}", "kind": "goal", "text": "structured item",
46
+ "source": f"event-{index}"}
47
+ for index in range(item_count)
48
+ ],
49
+ "dependencies": [],
50
+ "observations": [],
51
+ },
52
+ }
53
+
54
+
55
+ def test_handshake_authentication_and_active_session_switching(tmp_path):
56
+ source = ClientStateSource(tmp_path / "sessions", Weights())
57
+ server, launch = make_server(Store(), source=source)
58
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
59
+ thread.start()
60
+ base = launch.rstrip("/")
61
+ auth = {"Authorization": f"Bearer {server.write_token}"}
62
+
63
+ def get(path, headers=None):
64
+ with urlopen(Request(base + path, headers=headers or {}), timeout=5) as response:
65
+ return json.load(response)
66
+
67
+ def post(path, value):
68
+ request = Request(base + path, json.dumps(value).encode(),
69
+ {**auth, "Content-Type": "application/json"})
70
+ with urlopen(request, timeout=5) as response:
71
+ return json.load(response)
72
+
73
+ try:
74
+ with pytest.raises(HTTPError) as error:
75
+ get("/api/client/handshake")
76
+ assert error.value.code == 401
77
+ hello = get("/api/client/handshake", auth)
78
+ assert hello == {
79
+ "service": "xrefkit-attention-pet",
80
+ "protocolVersion": PROTOCOL_VERSION,
81
+ "instanceId": server.instance_id,
82
+ "capabilities": ["active-session", "working-set-snapshot"],
83
+ "writeAuthentication": "bearer",
84
+ "hostScope": "loopback",
85
+ }
86
+
87
+ accepted = post("/api/active-session", payload("session-a", 1, 1))
88
+ assert accepted["accepted"] is True
89
+ assert accepted["instanceId"] == server.instance_id
90
+ assert post("/api/active-session", payload("session-a", 1, 1)) == accepted
91
+ first = get("/api/state")
92
+ assert first["source"]["sessionId"] == "session-a"
93
+ assert first["source"]["activationRevision"] == 1
94
+ assert first["source"]["profile"] == "sol"
95
+ assert first["state"]["features"]["active_items"] == 1
96
+ english = get("/api/state?lang=en")
97
+ assert english["fit"]["presentation"]["headline"].startswith("For the work currently visible")
98
+ assert english["fit"]["presentation"]["coverageLabel"] == "Partial"
99
+
100
+ with pytest.raises(HTTPError) as error:
101
+ get("/api/state?lang=fr")
102
+ assert error.value.code == 400
103
+
104
+ next_session = payload("session-b", 2, 2)
105
+ next_session["model"] = "gpt-6-astra"
106
+ next_session["reasoning"] = "high"
107
+ post("/api/active-session", next_session)
108
+ second = get("/api/state?model=&reasoning=medium")
109
+ assert second["source"]["sessionId"] == "session-b"
110
+ assert second["fit"]["selectedProfile"] == {"model": "astra", "reasoning": "high"}
111
+ assert second["state"]["features"]["active_items"] == 2
112
+ manual_comparison = get("/api/state?model=sol&reasoning=medium")
113
+ assert manual_comparison["fit"]["selectedProfile"] == {"model": "sol", "reasoning": "medium"}
114
+
115
+ with pytest.raises(HTTPError) as error:
116
+ post("/api/active-session", payload("session-a", 1, 1))
117
+ assert error.value.code == 409
118
+ assert get("/api/state")["source"]["sessionId"] == "session-b"
119
+ finally:
120
+ server.shutdown()
121
+ server.server_close()
122
+ thread.join(timeout=5)
123
+
124
+
125
+ def test_state_response_keeps_one_session_when_activation_follows_snapshot(tmp_path, monkeypatch):
126
+ source = ClientStateSource(tmp_path / "sessions", Weights())
127
+ source.activate(ClientState.model_validate(payload("session-a", 1)))
128
+ next_session = payload("session-b", 2)
129
+ next_session["model"] = "gpt-6-astra"
130
+ next_session["reasoning"] = "high"
131
+ next_state = ClientState.model_validate(next_session)
132
+ original_snapshot = source.snapshot
133
+
134
+ def switch_after_snapshot(fallback_store):
135
+ snapshot = original_snapshot(fallback_store)
136
+ source.activate(next_state)
137
+ return snapshot
138
+
139
+ monkeypatch.setattr(source, "snapshot", switch_after_snapshot)
140
+ server, launch = make_server(Store(), source=source)
141
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
142
+ thread.start()
143
+ try:
144
+ with urlopen(launch + "api/state?model=&reasoning=medium", timeout=5) as response:
145
+ before = json.load(response)
146
+ assert before["state"]["taskId"] == "task-session-a"
147
+ assert before["source"]["sessionId"] == "session-a"
148
+ assert before["fit"]["selectedProfile"] == {"model": "sol", "reasoning": "medium"}
149
+
150
+ monkeypatch.setattr(source, "snapshot", original_snapshot)
151
+ with urlopen(launch + "api/state?model=&reasoning=medium", timeout=5) as response:
152
+ after = json.load(response)
153
+ assert after["state"]["taskId"] == "task-session-b"
154
+ assert after["source"]["sessionId"] == "session-b"
155
+ assert after["fit"]["selectedProfile"] == {"model": "astra", "reasoning": "high"}
156
+ finally:
157
+ server.shutdown()
158
+ server.server_close()
159
+ thread.join(timeout=5)
160
+
161
+
162
+ def test_client_snapshot_blocks_activation_until_all_session_fields_are_read(tmp_path, monkeypatch):
163
+ source = ClientStateSource(tmp_path / "sessions", Weights())
164
+ source.activate(ClientState.model_validate(payload("session-a", 1)))
165
+ next_session = payload("session-b", 2)
166
+ next_session["model"] = "gpt-6-astra"
167
+ next_session["reasoning"] = "high"
168
+ next_state = ClientState.model_validate(next_session)
169
+ view_read = threading.Event()
170
+ resume_view = threading.Event()
171
+ activation_started = threading.Event()
172
+ activation_done = threading.Event()
173
+ captured = {}
174
+ original_view = source.view
175
+
176
+ def pause_after_view(fallback_store):
177
+ result = original_view(fallback_store)
178
+ view_read.set()
179
+ assert resume_view.wait(timeout=5)
180
+ return result
181
+
182
+ def read_snapshot():
183
+ captured["snapshot"] = source.snapshot(Store())
184
+
185
+ def activate_next():
186
+ activation_started.set()
187
+ source.activate(next_state)
188
+ activation_done.set()
189
+
190
+ monkeypatch.setattr(source, "view", pause_after_view)
191
+ reader = threading.Thread(target=read_snapshot)
192
+ activator = threading.Thread(target=activate_next)
193
+ reader.start()
194
+ try:
195
+ assert view_read.wait(timeout=5)
196
+ activator.start()
197
+ assert activation_started.wait(timeout=5)
198
+ assert not activation_done.wait(timeout=0.1)
199
+ finally:
200
+ resume_view.set()
201
+ reader.join(timeout=5)
202
+ if activator.ident is not None:
203
+ activator.join(timeout=5)
204
+
205
+ assert not reader.is_alive() and not activator.is_alive()
206
+ result, selection, status = captured["snapshot"]
207
+ assert result["state"]["taskId"] == "task-session-a"
208
+ assert selection == ("sol", "medium")
209
+ assert status["sessionId"] == "session-a"
210
+ assert activation_done.is_set()
211
+ assert source.status()["sessionId"] == "session-b"
212
+
213
+
214
+ def test_client_contract_rejects_mismatched_session_and_manual_mode(tmp_path):
215
+ source = ClientStateSource(tmp_path / "sessions", Weights())
216
+ server, launch = make_server(Store(), source=source)
217
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
218
+ thread.start()
219
+ base = launch.rstrip("/")
220
+ auth = {"Authorization": f"Bearer {server.write_token}", "Content-Type": "application/json"}
221
+ try:
222
+ invalid = payload("session-a", 1)
223
+ invalid["workingSet"]["contextId"] = "different"
224
+ request = Request(base + "/api/active-session", json.dumps(invalid).encode(), auth)
225
+ with pytest.raises(HTTPError) as error:
226
+ urlopen(request, timeout=5)
227
+ assert error.value.code == 400
228
+ finally:
229
+ server.shutdown()
230
+ server.server_close()
231
+ thread.join(timeout=5)
232
+
233
+
234
+ def test_provider_client_verifies_loopback_identity_and_acknowledgement(tmp_path):
235
+ source = ClientStateSource(tmp_path / "sessions", Weights())
236
+ server, launch = make_server(Store(), source=source)
237
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
238
+ thread.start()
239
+ try:
240
+ client = AttentionPetClient(launch, server.write_token,
241
+ instance_id=server.instance_id)
242
+ assert client.handshake()["service"] == "xrefkit-attention-pet"
243
+ state = ClientState.model_validate(payload("session-client", 3))
244
+ assert client.activate(state)["activeSessionId"] == "session-client"
245
+
246
+ with pytest.raises(ClientProtocolError, match="instance"):
247
+ AttentionPetClient(launch, server.write_token,
248
+ instance_id="different").handshake()
249
+ with pytest.raises(ClientProtocolError, match="request failed"):
250
+ AttentionPetClient(launch, "wrong-token").handshake()
251
+ finally:
252
+ server.shutdown()
253
+ server.server_close()
254
+ thread.join(timeout=5)
255
+
256
+ for invalid in ["https://127.0.0.1:8769", "http://example.com:8769",
257
+ "http://127.0.0.1", "http://127.0.0.1:8769/?token=value"]:
258
+ with pytest.raises(ValueError, match="127.0.0.1"):
259
+ AttentionPetClient(invalid, "token")
260
+
261
+ manual, manual_launch = make_server(Store())
262
+ thread = threading.Thread(target=manual.serve_forever, daemon=True)
263
+ thread.start()
264
+ try:
265
+ request = Request(manual_launch.rstrip("/") + "/api/active-session",
266
+ json.dumps(payload("session-a", 1)).encode(),
267
+ {"Authorization": f"Bearer {manual.write_token}",
268
+ "Content-Type": "application/json"})
269
+ with pytest.raises(HTTPError) as error:
270
+ urlopen(request, timeout=5)
271
+ assert error.value.code == 409
272
+ finally:
273
+ manual.shutdown()
274
+ manual.server_close()
275
+ thread.join(timeout=5)
276
+
277
+
278
+ def test_client_schema_and_launch_record_are_machine_readable(tmp_path):
279
+ root = Path(__file__).resolve().parents[1]
280
+ schema = json.loads((root / "projects/attention-pet/schema/client-state.schema.json").read_text(encoding="utf-8"))
281
+ assert schema == ClientState.model_json_schema()
282
+ packaged = json.loads((root / "xrefkit/resources/attention_pet/client-state.schema.json").read_text(encoding="utf-8"))
283
+ assert packaged == schema
284
+
285
+ process = subprocess.Popen(
286
+ [sys.executable, "-m", "xrefkit.attention_pet", "serve", "--client",
287
+ "--port", "0", "--session", str(tmp_path / "sessions")],
288
+ cwd=root, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
289
+ encoding="utf-8",
290
+ )
291
+ try:
292
+ launch = json.loads(process.stdout.readline())
293
+ assert launch["service"] == "xrefkit-attention-pet"
294
+ assert launch["protocolVersion"] == PROTOCOL_VERSION
295
+ assert launch["endpoint"].startswith("http://127.0.0.1:")
296
+ assert launch["writeToken"]
297
+ client = AttentionPetClient(launch["endpoint"], launch["writeToken"],
298
+ instance_id=launch["instanceId"])
299
+ assert client.handshake()["instanceId"] == launch["instanceId"]
300
+ finally:
301
+ process.terminate()
302
+ process.wait(timeout=10)