xrefkit 0.5.1__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. {xrefkit-0.5.1 → xrefkit-0.6.1}/PKG-INFO +22 -3
  2. {xrefkit-0.5.1 → xrefkit-0.6.1}/README.md +21 -2
  3. {xrefkit-0.5.1 → xrefkit-0.6.1}/pyproject.toml +1 -1
  4. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_attention_pet_client_protocol.py +97 -2
  5. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_attention_pet_codex_session.py +3 -3
  6. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_attention_pet_fit.py +69 -12
  7. xrefkit-0.6.1/tests/test_legacy_migration.py +231 -0
  8. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/__init__.py +1 -1
  9. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/client_protocol.py +7 -2
  10. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/codex_session.py +2 -2
  11. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/evaluator.py +31 -21
  12. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/model.py +21 -0
  13. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/presentation.py +10 -10
  14. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/profiles.py +25 -5
  15. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/server.py +30 -9
  16. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/__init__.py +4 -0
  17. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/catalog.py +4 -0
  18. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/contracts.py +22 -0
  19. xrefkit-0.6.1/xrefkit/mcp/legacy_migration.py +383 -0
  20. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/schemas.py +2 -0
  21. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/server.py +28 -0
  22. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/setup.py +27 -34
  23. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/attention_pet/pet.css +2 -2
  24. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/attention_pet/pet.html +9 -9
  25. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/attention_pet/pet.js +58 -16
  26. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit.egg-info/PKG-INFO +22 -3
  27. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit.egg-info/SOURCES.txt +2 -0
  28. {xrefkit-0.5.1 → xrefkit-0.6.1}/LICENSE +0 -0
  29. {xrefkit-0.5.1 → xrefkit-0.6.1}/setup.cfg +0 -0
  30. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_artifact_selected_family.py +0 -0
  31. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_attention_pet.py +0 -0
  32. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_boundary_analysis.py +0 -0
  33. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_brownfield_csharp_structure_definitions.py +0 -0
  34. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_brownfield_file_editing_protocol.py +0 -0
  35. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_business_intake_conversation_definitions.py +0 -0
  36. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_business_intake_definition.py +0 -0
  37. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_calibration_lint.py +0 -0
  38. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_catalog_preparation_definition.py +0 -0
  39. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_check_skill_knowledge_xids.py +0 -0
  40. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_cli.py +0 -0
  41. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_code_constraint_definition.py +0 -0
  42. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_collect_analyzer_sarif.py +0 -0
  43. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_constraint_derivation_family_definition.py +0 -0
  44. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_constraint_derivation_selected_family.py +0 -0
  45. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_contribution_returns.py +0 -0
  46. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_convert_to_xrefkit_skill.py +0 -0
  47. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_cs_scope_probe.py +0 -0
  48. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_csharp_commonality.py +0 -0
  49. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_csharp_naming_profile.py +0 -0
  50. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_ctx.py +0 -0
  51. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_cutover_readiness.py +0 -0
  52. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_dashboard.py +0 -0
  53. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_database_batch_definition_migration.py +0 -0
  54. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_decision_trace.py +0 -0
  55. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_design_business_intake_selected_family.py +0 -0
  56. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_design_planning_definition_migration.py +0 -0
  57. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_dotnet_definition_candidate.py +0 -0
  58. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_editorial_family_definition.py +0 -0
  59. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_editorial_intake_definition.py +0 -0
  60. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_editorial_selected_family.py +0 -0
  61. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_embedded_startup_pack.py +0 -0
  62. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_error_policy_audit.py +0 -0
  63. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_error_policy_locator.py +0 -0
  64. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_execution_binding.py +0 -0
  65. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_existing_skill_definition_batch.py +0 -0
  66. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_final_skilldefinition_batch.py +0 -0
  67. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_flow_selected_family.py +0 -0
  68. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_fm_multiroot.py +0 -0
  69. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_gate.py +0 -0
  70. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_gateway.py +0 -0
  71. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_gateway_mcp.py +0 -0
  72. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_gateway_skill_adapter.py +0 -0
  73. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_gateway_work_items.py +0 -0
  74. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_goal_desired_state.py +0 -0
  75. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_host_precheck.py +0 -0
  76. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_human_evaluation.py +0 -0
  77. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_implementation_flow_definition.py +0 -0
  78. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_implementation_review_selected_family.py +0 -0
  79. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_inbound_uploads.py +0 -0
  80. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_instruction_workflow.py +0 -0
  81. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_knowledge_relations_validator.py +0 -0
  82. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_language_review_definition.py +0 -0
  83. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_mcp_package_routing.py +0 -0
  84. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_mcp_setup.py +0 -0
  85. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_mcp_subagent_startup.py +0 -0
  86. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_mcp_subagent_startup_integration.py +0 -0
  87. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_os_family_definition.py +0 -0
  88. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_os_knowledge_family_definition.py +0 -0
  89. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_os_selected_family.py +0 -0
  90. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_ownership.py +0 -0
  91. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_packmeta.py +0 -0
  92. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_project_quality_baseline.py +0 -0
  93. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_qa_report_definition.py +0 -0
  94. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_resource_provider.py +0 -0
  95. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_runtime_contracts.py +0 -0
  96. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_sarif_to_locator.py +0 -0
  97. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_security_definition_migration.py +0 -0
  98. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_definition.py +0 -0
  99. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_definition_governance.py +0 -0
  100. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_definition_mcp.py +0 -0
  101. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_definition_runtime.py +0 -0
  102. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_edits.py +0 -0
  103. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_maturity_return.py +0 -0
  104. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skill_runtime_audit.py +0 -0
  105. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skillmeta.py +0 -0
  106. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_skills_sync.py +0 -0
  107. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_structure_catalog.py +0 -0
  108. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_subagent_startup.py +0 -0
  109. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_subagent_startup_boundaries.py +0 -0
  110. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_wbs.py +0 -0
  111. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_workflow_family_definition.py +0 -0
  112. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_xref.py +0 -0
  113. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_xrefkit_instance.py +0 -0
  114. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_xrefkit_tools.py +0 -0
  115. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_xrefkit_v2_discovery.py +0 -0
  116. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_xrefkit_v2_models.py +0 -0
  117. {xrefkit-0.5.1 → xrefkit-0.6.1}/tests/test_xrefkit_v2_pipeline.py +0 -0
  118. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/__main__.py +0 -0
  119. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/__init__.py +0 -0
  120. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/__main__.py +0 -0
  121. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/client.py +0 -0
  122. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/scenarios.py +0 -0
  123. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/attention_pet/store.py +0 -0
  124. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/boundary_analysis.py +0 -0
  125. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/catalog_cli.py +0 -0
  126. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/cli.py +0 -0
  127. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/contracts.py +0 -0
  128. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/ctx.py +0 -0
  129. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/dashboard.py +0 -0
  130. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/decision_trace.py +0 -0
  131. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/discovery.py +0 -0
  132. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/execution_binding.py +0 -0
  133. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/execution_binding_cli.py +0 -0
  134. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/gate.py +0 -0
  135. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/gateway.py +0 -0
  136. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/goalstate.py +0 -0
  137. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/hashing.py +0 -0
  138. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/host_precheck.py +0 -0
  139. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/import_skill.py +0 -0
  140. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/instance.py +0 -0
  141. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/loaders.py +0 -0
  142. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/audit.py +0 -0
  143. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/bootstrap.py +0 -0
  144. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/cli.py +0 -0
  145. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/client_cache.py +0 -0
  146. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/client_flow.py +0 -0
  147. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/context_registry.py +0 -0
  148. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/context_token.py +0 -0
  149. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/contribution_adoption.py +0 -0
  150. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/contribution_returns.py +0 -0
  151. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/dist.py +0 -0
  152. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/gateway.py +0 -0
  153. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/inbound_uploads.py +0 -0
  154. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/knowledge_edits.py +0 -0
  155. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/ownership.py +0 -0
  156. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/repository.py +0 -0
  157. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/skill_edits.py +0 -0
  158. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/skill_maturity.py +0 -0
  159. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/startup_contract_pack.py +0 -0
  160. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp/subagent_startup.py +0 -0
  161. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/mcp_tools.py +0 -0
  162. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/__init__.py +0 -0
  163. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/common.py +0 -0
  164. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/effective_bundle.py +0 -0
  165. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/human_evaluation.py +0 -0
  166. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/local_manifest.py +0 -0
  167. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/package_manifest.py +0 -0
  168. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/run_log.py +0 -0
  169. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/server_config.py +0 -0
  170. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/models/skill_definition.py +0 -0
  171. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/operations_cli.py +0 -0
  172. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/ownership.py +0 -0
  173. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/packmeta.py +0 -0
  174. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/registry.py +0 -0
  175. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resolver.py +0 -0
  176. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resource_provider.py +0 -0
  177. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/attention_pet/client-state.schema.json +0 -0
  178. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/contracts.json +0 -0
  179. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/current.json +0 -0
  180. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
  181. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
  182. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
  183. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
  184. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/model_body.md +0 -0
  185. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/startup_sources/000_agent_entry.md +0 -0
  186. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/startup_sources/011_startup_xref_routing.md +0 -0
  187. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/startup_sources/015_shared_memory_operations.md +0 -0
  188. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/startup_sources/016_uncertainty_protocol.md +0 -0
  189. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/startup_sources/017_base_and_xref_layering.md +0 -0
  190. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/resources/base/startup_sources/053_context_direction_security_guard.md +0 -0
  191. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/runlog.py +0 -0
  192. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/skill_definition.py +0 -0
  193. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/skill_definition_catalog.py +0 -0
  194. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/skill_definition_governance.py +0 -0
  195. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/skillmeta.py +0 -0
  196. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/skillrun.py +0 -0
  197. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/skills_sync.py +0 -0
  198. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/structure_catalog.py +0 -0
  199. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/subagent_startup.py +0 -0
  200. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/tools/__init__.py +0 -0
  201. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/tools/__main__.py +0 -0
  202. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/v2_cli.py +0 -0
  203. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/wbs.py +0 -0
  204. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/workspace.py +0 -0
  205. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit/xref.py +0 -0
  206. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit.egg-info/dependency_links.txt +0 -0
  207. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit.egg-info/entry_points.txt +0 -0
  208. {xrefkit-0.5.1 → xrefkit-0.6.1}/xrefkit.egg-info/requires.txt +0 -0
  209. {xrefkit-0.5.1 → xrefkit-0.6.1}/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.1
3
+ Version: 0.6.1
4
4
  Summary: Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit
5
5
  Author: synthaicode
6
6
  License: MIT License
@@ -47,7 +47,7 @@ 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.md) helps
50
+ Experimental local tool: [Attention Pet](projects/attention-pet/README.en.md) helps
51
51
  people assess capability sufficiency and lower inference-cost candidates separately.
52
52
  Its Model Fit / Cost Fit estimates use uncalibrated profiles; it does not measure internal
53
53
  attention or switch models automatically.
@@ -61,6 +61,11 @@ pet. It also shows lower inference-cost comparison candidates when the
61
61
  experimental profiles support one. The estimate is observational and does not
62
62
  change the Codex model, reasoning level, or chat.
63
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
+
64
69
  Run it from a Codex terminal in this repository so `CODEX_THREAD_ID` identifies
65
70
  the current chat:
66
71
 
@@ -77,7 +82,7 @@ This Codex preview is bound to the chat that launched it. Selecting another
77
82
  chat does not move the Pet automatically; launch it again from that chat when
78
83
  you want a separate view. The model and reasoning dropdowns only compare
79
84
  estimates and do not change the active Codex settings. See the
80
- [Attention Pet guide](projects/attention-pet/README.md) for interpretation,
85
+ [Attention Pet guide](projects/attention-pet/README.en.md) for interpretation,
81
86
  limitations, client integration, and API details.
82
87
 
83
88
  Using AI for real work creates recurring operating problems:
@@ -151,6 +156,20 @@ This command starts the optional catalog and runtime MCP adapter. This checkout
151
156
  does not implement management upload, staging, sealing, review, or adoption of
152
157
  uploaded SkillDefinition bytes into the active catalog.
153
158
 
159
+ To connect VS Code/Copilot, run from your target workspace:
160
+
161
+ ```powershell
162
+ python -m pip install "xrefkit[mcp]"
163
+ python -m xrefkit mcp setup --repo . --output .xrefkit/setup-review
164
+ python -m xrefkit mcp setup apply --repo . --source .xrefkit/setup-review
165
+ ```
166
+
167
+ Review the generated files before applying them, open the workspace in VS Code,
168
+ and start the `xrefkit` MCP server. In Copilot chat, ask: "Call XRefKit's
169
+ get_startup_context and help me get started." The AI receives any needed legacy
170
+ migration guidance; you can describe your goal without specifying Skill names.
171
+ Opening VS Code alone does not guarantee an AI conversation starts.
172
+
154
173
  ## Where to go next
155
174
 
156
175
  - [Install XRefKit and register Skill Packages](docs/guides/089_xrefkit_package_first_registration.md#xid-4F8C2A7D1E90)
@@ -8,7 +8,7 @@ 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.md) helps
11
+ Experimental local tool: [Attention Pet](projects/attention-pet/README.en.md) helps
12
12
  people assess capability sufficiency and lower inference-cost candidates separately.
13
13
  Its Model Fit / Cost Fit estimates use uncalibrated profiles; it does not measure internal
14
14
  attention or switch models automatically.
@@ -22,6 +22,11 @@ pet. It also shows lower inference-cost comparison candidates when the
22
22
  experimental profiles support one. The estimate is observational and does not
23
23
  change the Codex model, reasoning level, or chat.
24
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
+
25
30
  Run it from a Codex terminal in this repository so `CODEX_THREAD_ID` identifies
26
31
  the current chat:
27
32
 
@@ -38,7 +43,7 @@ This Codex preview is bound to the chat that launched it. Selecting another
38
43
  chat does not move the Pet automatically; launch it again from that chat when
39
44
  you want a separate view. The model and reasoning dropdowns only compare
40
45
  estimates and do not change the active Codex settings. See the
41
- [Attention Pet guide](projects/attention-pet/README.md) for interpretation,
46
+ [Attention Pet guide](projects/attention-pet/README.en.md) for interpretation,
42
47
  limitations, client integration, and API details.
43
48
 
44
49
  Using AI for real work creates recurring operating problems:
@@ -112,6 +117,20 @@ This command starts the optional catalog and runtime MCP adapter. This checkout
112
117
  does not implement management upload, staging, sealing, review, or adoption of
113
118
  uploaded SkillDefinition bytes into the active catalog.
114
119
 
120
+ To connect VS Code/Copilot, run from your target workspace:
121
+
122
+ ```powershell
123
+ python -m pip install "xrefkit[mcp]"
124
+ python -m xrefkit mcp setup --repo . --output .xrefkit/setup-review
125
+ python -m xrefkit mcp setup apply --repo . --source .xrefkit/setup-review
126
+ ```
127
+
128
+ Review the generated files before applying them, open the workspace in VS Code,
129
+ and start the `xrefkit` MCP server. In Copilot chat, ask: "Call XRefKit's
130
+ get_startup_context and help me get started." The AI receives any needed legacy
131
+ migration guidance; you can describe your goal without specifying Skill names.
132
+ Opening VS Code alone does not guarantee an AI conversation starts.
133
+
115
134
  ## Where to go next
116
135
 
117
136
  - [Install XRefKit and register Skill Packages](docs/guides/089_xrefkit_package_first_registration.md#xid-4F8C2A7D1E90)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xrefkit"
7
- version = "0.5.1"
7
+ version = "0.6.1"
8
8
  description = "Portable XID, Skill, Knowledge, workflow, and MCP runtime for XRefKit"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -101,10 +101,16 @@ def test_handshake_authentication_and_active_session_switching(tmp_path):
101
101
  get("/api/state?lang=fr")
102
102
  assert error.value.code == 400
103
103
 
104
- post("/api/active-session", payload("session-b", 2, 2))
105
- second = get("/api/state")
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")
106
109
  assert second["source"]["sessionId"] == "session-b"
110
+ assert second["fit"]["selectedProfile"] == {"model": "astra", "reasoning": "high"}
107
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"}
108
114
 
109
115
  with pytest.raises(HTTPError) as error:
110
116
  post("/api/active-session", payload("session-a", 1, 1))
@@ -116,6 +122,95 @@ def test_handshake_authentication_and_active_session_switching(tmp_path):
116
122
  thread.join(timeout=5)
117
123
 
118
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
+
119
214
  def test_client_contract_rejects_mismatched_session_and_manual_mode(tmp_path):
120
215
  source = ClientStateSource(tmp_path / "sessions", Weights())
121
216
  server, launch = make_server(Store(), source=source)
@@ -38,7 +38,7 @@ def test_one_bound_chat_updates_incrementally_without_saving_text(tmp_path):
38
38
  store = Store(saved)
39
39
  source.sync(store)
40
40
  first = store.view()
41
- assert source.selection() == ("sol", "standard")
41
+ assert source.selection() == ("sol", "medium")
42
42
  assert source.status()["observedUserTurns"] == 1
43
43
  assert first["state"]["coverage"] == "partial"
44
44
  assert len(first["workingSet"]["items"]) == 1
@@ -55,7 +55,7 @@ def test_one_bound_chat_updates_incrementally_without_saving_text(tmp_path):
55
55
  assert "この修正は不要" not in saved.read_text(encoding="utf-8")
56
56
  append(log, "2026-09-26T12:00:03Z", "turn_context", {"model": "gpt-6-terra", "effort": "low"})
57
57
  source.sync(store)
58
- assert source.selection() == ("terra", "light")
58
+ assert source.selection() == ("terra", "low")
59
59
  assert store.view() == second
60
60
  append(log, "2026-09-26T12:00:04Z", "turn_context", {"model": "unrecognized-model", "effort": "medium"})
61
61
  source.sync(store)
@@ -80,7 +80,7 @@ def test_live_api_defaults_to_observed_model_allows_comparison_and_rejects_mutat
80
80
  assert value["source"]["mode"] == "codex-chat"
81
81
  assert value["source"]["observedUserTurns"] == 1
82
82
  assert value["source"]["profile"] == "sol"
83
- assert value["source"]["reasoning"] == "standard"
83
+ assert value["source"]["reasoning"] == "medium"
84
84
  assert value["fit"]["selected"]["model"] == "astra"
85
85
  assert value["fit"]["selected"]["reasoning"] == "high"
86
86
  with urlopen(Request(base + "/api/state?model=&reasoning=standard"), timeout=5) as response:
@@ -16,6 +16,50 @@ from xrefkit.attention_pet.server import make_server
16
16
  from xrefkit.attention_pet.store import Store
17
17
 
18
18
 
19
+ def test_execution_profile_search_crosses_models_and_efforts(monkeypatch):
20
+ # Controlled hypotheses make the two cross-axis cases independent of scenario thresholds.
21
+ state = {"ral": 50, "causes": [], "features": {"active_items": 1},
22
+ "coverage": "reviewed", "trajectoryEvidence": []}
23
+ for model, base in (("luna", 20), ("sol", 40)):
24
+ monkeypatch.setitem(PROFILES, model, PROFILES[model].model_copy(
25
+ update={"capability": Capability(reasoning=base, constraint_tracking=base,
26
+ evidence_handling=base)}))
27
+ for model in ("terra", "astra"):
28
+ monkeypatch.setitem(PROFILES, model, PROFILES[model].model_copy(
29
+ update={"relative_inference_cost": 100.0}))
30
+ result = evaluate_fit(state, "sol", "high")
31
+ assert result["modelFit"] == "Sufficient"
32
+ assert result["costFit"] == "LowerCostCandidateAvailable"
33
+ assert { (c["model"], c["reasoning"]) for c in result["lowerCostCandidates"] } >= {
34
+ ("luna", "xhigh"), ("sol", "medium")}
35
+ assert not next(c for c in result["alternatives"] if
36
+ (c["model"], c["reasoning"]) == ("luna", "high"))["meetsRequirements"]
37
+ assert result["lowerCostCandidate"] == {"model": "sol", "reasoning": "medium"}
38
+ assert result["selectedProfile"] == {"model": "sol", "reasoning": "high"}
39
+ assert result["expectedTotalCost"] is None
40
+
41
+ underpowered = evaluate_fit(state, "sol", "low")
42
+ assert underpowered["modelFit"] == "Underpowered"
43
+ assert underpowered["costFit"] == "RetryRisk"
44
+ assert underpowered["lowerCostCandidate"] is None
45
+ assert underpowered["lowerCostCandidates"] == []
46
+
47
+
48
+ def test_unknown_and_legacy_execution_profiles():
49
+ state = evaluate(task())
50
+ for model, reasoning in (("absent", "high"), ("sol", "absent"),
51
+ ("terra", "xhigh")):
52
+ result = evaluate_fit(state, model, reasoning)
53
+ assert result["modelFit"] == result["costFit"] == "Unknown"
54
+ assert result["presentation"]["petState"] == "Unknown"
55
+ assert evaluate_fit(state, "sol", "light")["selected"]["reasoning"] == "light"
56
+ assert evaluate_fit(state, "sol", "standard")["selected"]["reasoning"] == "standard"
57
+ assert evaluate_fit(state, "sol", "light")["selectedProfile"] == {"model": "sol", "reasoning": "low"}
58
+ assert evaluate_fit(state, "sol", "standard")["selectedProfile"] == {"model": "sol", "reasoning": "medium"}
59
+ assert evaluate_fit(state, "sol", "standard")["selected"]["capability"] == \
60
+ evaluate_fit(state, "sol", "medium")["selected"]["capability"]
61
+
62
+
19
63
  def task(name="A", step=5):
20
64
  return WorkingSet.model_validate(scenario(name, step)["workingSet"])
21
65
 
@@ -70,10 +114,10 @@ def test_comparison_is_pure_and_total_cost_is_unknown():
70
114
  assert {r["baseRal"] for r in results} == {state["ral"]}
71
115
  for result in results:
72
116
  assert result["expectedTotalCost"] is None
73
- assert all(result[k] is None for k in ("inferenceCost", "retryCost", "correctionCost", "failureRiskCost"))
117
+ assert all(result[k] is None for k in ("inferenceCost", "retryCost", "correctionCost", "latencyCost", "failureRiskCost"))
74
118
  assert result["calibration"] == "uncalibrated"
75
119
  assert results[0]["costFit"] == "RetryRisk"
76
- assert results[1]["costFit"] == "NoLowerCostCandidate"
120
+ assert results[1]["costFit"] == "LowerCostCandidateAvailable"
77
121
  assert results[2]["costFit"] == "LowerCostCandidateAvailable"
78
122
 
79
123
 
@@ -121,9 +165,9 @@ def test_schema_matches_python_cost_fit_contract():
121
165
 
122
166
 
123
167
  @pytest.mark.parametrize("name,step,costs,faces", [
124
- ("A", 5, ["NoLowerCostCandidate"] + ["LowerCostCandidateAvailable"] * 3, ["Balanced"] + ["Relaxed"] * 3),
125
- ("B", 3, ["RetryRisk", "NoLowerCostCandidate", "LowerCostCandidateAvailable", "LowerCostCandidateAvailable"], ["Strained", "Balanced", "Relaxed", "Relaxed"]),
126
- ("B", 5, ["RetryRisk"] * 3 + ["NoLowerCostCandidate"], ["Strained"] * 3 + ["Balanced"]),
168
+ ("A", 5, ["LowerCostCandidateAvailable"] * 4, ["Relaxed"] * 4),
169
+ ("B", 3, ["RetryRisk"] + ["LowerCostCandidateAvailable"] * 3, ["Strained"] + ["Relaxed"] * 3),
170
+ ("B", 5, ["RetryRisk"] * 3 + ["LowerCostCandidateAvailable"], ["Strained"] * 3 + ["Relaxed"]),
127
171
  ("D", 5, ["RetryRisk"] * 4, ["Strained"] * 4),
128
172
  ("C", 3, ["ReviewNeeded"] * 4, ["Review"] * 4),
129
173
  ])
@@ -141,11 +185,11 @@ def test_scenarios_separate_cost_and_presentation(name, step, costs, faces):
141
185
 
142
186
  def test_other_prices_do_not_change_model_fit(monkeypatch):
143
187
  state = evaluate(task())
144
- before = evaluate_fit(state, "astra")
188
+ before = evaluate_fit(state, "astra", "low")
145
189
  assert before["costFit"] == "LowerCostCandidateAvailable"
146
190
  for name in ("luna", "terra", "sol"):
147
191
  monkeypatch.setitem(PROFILES, name, PROFILES[name].model_copy(update={"relative_inference_cost": 100.0}))
148
- after = evaluate_fit(state, "astra")
192
+ after = evaluate_fit(state, "astra", "low")
149
193
  assert before["modelFit"] == after["modelFit"] == "Sufficient"
150
194
  assert after["costFit"] == "NoLowerCostCandidate"
151
195
  assert after["lowerCostCandidates"] == []
@@ -153,9 +197,9 @@ def test_other_prices_do_not_change_model_fit(monkeypatch):
153
197
 
154
198
  @pytest.mark.parametrize("name,step,model,status,target", [
155
199
  ("A", 5, "astra", "Available", "Luna"),
156
- ("B", 3, "astra", "Available", "Terra"),
157
- ("B", 3, "sol", "Available", "Terra"),
158
- ("B", 3, "terra", "NoLowerCandidate", None),
200
+ ("B", 3, "astra", "Available", "Sol"),
201
+ ("B", 3, "sol", "Available", "Sol"),
202
+ ("B", 3, "terra", "Available", "Sol"),
159
203
  ("B", 3, "luna", "Underpowered", None),
160
204
  ("C", 3, "astra", "HoldForReview", None),
161
205
  ("D", 5, "astra", "Underpowered", None),
@@ -168,7 +212,7 @@ def test_lowest_sufficient_downgrade_guidance(name, step, model, status, target)
168
212
  if target:
169
213
  assert f"低コスト比較候補: {target}" in guide["modelGuide"]
170
214
  assert target in guide["modelGuideShort"]
171
- assert "同じ考える深さ" in guide["modelGuide"]
215
+ assert " / " in guide["modelGuideShort"]
172
216
  assert "再試行・修正時間・失敗損失を含む総コストは未比較" in guide["modelGuideDetail"]
173
217
  assert "仮の必要能力3軸を満たす試算" in guide["modelGuideDetail"]
174
218
  lowest = min(fit["lowerCostCandidates"], key=lambda c: c["relativeInferenceCost"])
@@ -241,7 +285,7 @@ def test_english_presentation_preserves_evaluation_meaning():
241
285
  ("Unknown", "Unknown", "Unknown", "まだ評価できません", "情報が不足"),
242
286
  ("Underpowered", "RetryRisk", "Strained", "能力が不足する可能性", "一部を満たしていません"),
243
287
  ("Sufficient", "NoLowerCostCandidate", "Balanced", "必要な能力を満たす試算", "候補は確認されていません"),
244
- ("Sufficient", "LowerCostCandidateAvailable", "Relaxed", "より低い推論コストの候補", "現在のモデルでも必要能力を満たす試算"),
288
+ ("Sufficient", "LowerCostCandidateAvailable", "Relaxed", "より低い推論コストの候補", "現在の実行プロファイルでも必要能力を満たす試算"),
245
289
  ("Sufficient", "ReviewNeeded", "Review", "実際の結果", "モデル能力だけを原因とは判断していません"),
246
290
  ("Underpowered", "ReviewNeeded", "Review", "実際の結果", "モデル能力だけを原因とは判断していません"),
247
291
  ])
@@ -309,6 +353,13 @@ def test_profiles_reject_invalid_costs(field, value):
309
353
  ModelProfile.model_validate(data)
310
354
 
311
355
 
356
+ def test_profiles_reject_reasoning_without_depth_definition():
357
+ data = PROFILES["luna"].model_dump()
358
+ data["supported_reasoning"] = ("low", "ultra")
359
+ with pytest.raises(ValidationError, match="unsupported reasoning levels: ultra"):
360
+ ModelProfile.model_validate(data)
361
+
362
+
312
363
  def test_api_model_selection_is_read_only_and_invalid_queries_cannot_mutate():
313
364
  store = Store()
314
365
  before = store.submit(task("B", 3))
@@ -324,6 +375,12 @@ def test_api_model_selection_is_read_only_and_invalid_queries_cannot_mutate():
324
375
  return json.load(response)
325
376
 
326
377
  try:
378
+ catalog = request("/api/profiles")
379
+ assert next(p for p in catalog["models"] if p["id"] == "luna")["reasoning"] == [
380
+ "low", "medium", "high", "xhigh"]
381
+ assert catalog["legacyAliases"] == {"light": "low", "standard": "medium"}
382
+ unknown = request("/api/state?model=absent&reasoning=xhigh")
383
+ assert unknown["fit"]["modelFit"] == "Unknown"
327
384
  for model, expected in zip(PROFILES, ["Underpowered", "Sufficient", "Sufficient", "Sufficient"], strict=True):
328
385
  value = request(f"/api/state?model={model}&reasoning=standard")
329
386
  assert value["fit"]["modelFit"] == expected
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import sys
6
+ import subprocess
7
+ import os
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from xrefkit.mcp.catalog import XRefCatalog
13
+ from xrefkit.mcp.legacy_migration import (
14
+ STATE, dismiss_legacy_migration, import_legacy_assets, migration_context,
15
+ plan_legacy_migration, record_mcp_retrieval,
16
+ )
17
+
18
+
19
+ def legacy(tmp_path: Path) -> tuple[Path, Path]:
20
+ source, root = tmp_path / "legacy", tmp_path / "target"
21
+ root.mkdir()
22
+ (source / "skills" / "nested" / "review").mkdir(parents=True)
23
+ (source / "knowledge").mkdir()
24
+ (source / "skills" / "nested" / "review" / "SKILL.md").write_text(
25
+ "# Review\n\nUse [rules](../../../knowledge/rules.md).\n", encoding="utf-8")
26
+ (source / "knowledge" / "rules.md").write_text("# Rules\n\nKeep evidence.\n", encoding="utf-8")
27
+ (source / "knowledge" / "unlinked.txt").write_text("Independent knowledge.\n", encoding="utf-8")
28
+ return source, root
29
+
30
+
31
+ def snapshot(root: Path) -> dict[str, bytes]:
32
+ return {p.relative_to(root).as_posix(): p.read_bytes() for p in root.rglob("*") if p.is_file()}
33
+
34
+
35
+ def documents(root: Path) -> list[dict]:
36
+ data = json.loads((root / STATE).read_text(encoding="utf-8"))
37
+ catalog = XRefCatalog.build(root)
38
+ return [catalog.get_document_by_xid(a["xid"]) for unit in data["units"].values()
39
+ for a in unit.get("artifacts", []) if unit["status"] == "installed"]
40
+
41
+
42
+ def test_first_partial_complete_restart_and_upgrade(tmp_path, monkeypatch):
43
+ source, root = legacy(tmp_path)
44
+ original = snapshot(source)
45
+ catalog = XRefCatalog.build(root)
46
+ first = catalog.get_startup_context()["legacy_migration"]
47
+ assert first["status"] == "not_started"
48
+ assert first["client_instructions"]
49
+ assert not (root / STATE).exists()
50
+ plan = plan_legacy_migration(root, str(source))
51
+ assert "knowledge/unlinked.txt" in plan["targets"]
52
+ assert "skills/nested/review" in plan["targets"]
53
+ assert not (root / STATE).exists() # inventory has no write side effect
54
+ result = import_legacy_assets(root, str(source), plan["targets"])
55
+ assert result["ok"]
56
+ assert migration_context(root)["status"] == "in_progress"
57
+ bodies = documents(root)
58
+ # Catalog/CLI verification does not masquerade as an MCP retrieval.
59
+ assert migration_context(root)["status"] == "in_progress"
60
+ record_mcp_retrieval(root, bodies[:1])
61
+ remaining = migration_context(root)
62
+ assert remaining["status"] == "in_progress"
63
+ assert sum(len(u["retrievals"]) for u in remaining["remaining"]) < len(bodies)
64
+ record_mcp_retrieval(root, bodies)
65
+ completed = migration_context(root)
66
+ assert completed["status"] == "completed"
67
+ assert completed["client_instructions"] == []
68
+ assert completed["remaining"] == []
69
+ state = (root / STATE).read_bytes()
70
+ import xrefkit.mcp
71
+ monkeypatch.setattr(xrefkit.mcp, "__version__", "999.0.0")
72
+ restarted = XRefCatalog.build(root).get_startup_context({"irrelevant": "cached"})
73
+ assert restarted["legacy_migration"]["status"] == "completed"
74
+ assert (root / STATE).read_bytes() == state
75
+ assert snapshot(source) == original
76
+
77
+
78
+ def test_explicit_scope_and_dismissal(tmp_path):
79
+ source, root = legacy(tmp_path)
80
+ assert dismiss_legacy_migration(root)["status"] == "not_required"
81
+ assert XRefCatalog.build(root).get_startup_context()["legacy_migration"]["client_instructions"] == []
82
+ result = import_legacy_assets(root, str(source), ["knowledge/unlinked.txt"])
83
+ assert result["ok"]
84
+ assert not (root / "skills").exists()
85
+ with pytest.raises(ValueError, match="cannot dismiss"):
86
+ dismiss_legacy_migration(root)
87
+ record_mcp_retrieval(root, documents(root))
88
+ assert migration_context(root)["status"] == "completed"
89
+
90
+
91
+ def test_conflict_failure_retry_and_existing_assets_protected(tmp_path):
92
+ source, root = legacy(tmp_path)
93
+ protected = root / "skills" / "imported.nested_review" / "SKILL.md"
94
+ protected.parent.mkdir(parents=True)
95
+ protected.write_bytes(b"Existing customized skill\n")
96
+ result = import_legacy_assets(root, str(source), ["skills/nested/review", "knowledge/unlinked.txt"], prefix="imported")
97
+ assert not result["ok"]
98
+ remaining = migration_context(root)["remaining"]
99
+ assert remaining[0]["status"] == "failed"
100
+ assert "refusing to overwrite" in remaining[0]["error"]
101
+ assert protected.read_bytes() == b"Existing customized skill\n"
102
+ record_mcp_retrieval(root, documents(root))
103
+ assert [u["target"] for u in migration_context(root)["remaining"]] == ["skills/nested/review"]
104
+ # Simulate a user moving their customized file aside after reviewing conflict.
105
+ protected.rename(protected.with_suffix(".backup"))
106
+ assert import_legacy_assets(root, str(source), ["skills/nested/review"], prefix="imported")["ok"]
107
+ record_mcp_retrieval(root, documents(root))
108
+ assert migration_context(root)["status"] == "completed"
109
+ assert protected.with_suffix(".backup").read_bytes() == b"Existing customized skill\n"
110
+
111
+
112
+ def test_partial_publish_retry_is_stable(tmp_path, monkeypatch):
113
+ source, root = legacy(tmp_path)
114
+ real_link = os.link
115
+ failed = False
116
+ def fail_once(source_path, destination, *args, **kwargs):
117
+ nonlocal failed
118
+ if Path(destination).name == "meta.md" and not failed:
119
+ failed = True
120
+ raise OSError("fixture disk failure")
121
+ return real_link(source_path, destination, *args, **kwargs)
122
+ monkeypatch.setattr(os, "link", fail_once)
123
+ assert not import_legacy_assets(root, str(source), ["skills/nested/review"])["ok"]
124
+ published = snapshot(root / "skills")
125
+ assert import_legacy_assets(root, str(source), ["skills/nested/review"])["ok"]
126
+ assert all(snapshot(root / "skills")[p] == b for p, b in published.items())
127
+ record_mcp_retrieval(root, documents(root))
128
+ assert migration_context(root)["status"] == "completed"
129
+
130
+
131
+ def test_receipts_require_matching_body_identity_and_content(tmp_path):
132
+ source, root = legacy(tmp_path)
133
+ import_legacy_assets(root, str(source), ["knowledge/unlinked.txt"])
134
+ body = documents(root)[0]
135
+ for wrong in ({**body, "content": None}, {**body, "content": "wrong"},
136
+ {**body, "path": "other.md"}, {**body, "xid": "wrong"}):
137
+ record_mcp_retrieval(root, [wrong])
138
+ assert migration_context(root)["status"] == "in_progress"
139
+ record_mcp_retrieval(root, [body])
140
+ assert migration_context(root)["status"] == "completed"
141
+ state = json.loads((root / STATE).read_text(encoding="utf-8"))
142
+ path = root / next(iter(state["units"].values()))["artifacts"][0]["path"]
143
+ path.write_text("Customized after migration", encoding="utf-8")
144
+ assert migration_context(root)["status"] == "in_progress"
145
+ assert not import_legacy_assets(root, str(source), ["knowledge/unlinked.txt"])["ok"]
146
+ assert path.read_text(encoding="utf-8") == "Customized after migration"
147
+
148
+
149
+ def test_corrupt_state_and_overlap_are_not_reset(tmp_path):
150
+ source, root = legacy(tmp_path)
151
+ for bad in ("{", '{"schema_version": 999, "units": {}}'):
152
+ (root / STATE).parent.mkdir(exist_ok=True)
153
+ (root / STATE).write_text(bad, encoding="utf-8")
154
+ assert migration_context(root)["status"] == "blocked"
155
+ with pytest.raises(ValueError):
156
+ import_legacy_assets(root, str(source), ["knowledge/unlinked.txt"])
157
+ assert (root / STATE).read_text(encoding="utf-8") == bad
158
+ for overlap in (root, root.parent):
159
+ with pytest.raises(ValueError, match="non-overlapping"):
160
+ plan_legacy_migration(root, str(overlap))
161
+ with pytest.raises(ValueError, match="exact targets"):
162
+ import_legacy_assets(root, str(source), ["../escape"])
163
+
164
+
165
+ def test_existing_xid_conflict_preserves_catalog_and_source(tmp_path):
166
+ source, root = legacy(tmp_path)
167
+ xid = "ABCDEF012345"
168
+ (source / "knowledge" / "rules.md").write_text(f"<!-- xid: {xid} -->\n# Legacy rules\n", encoding="utf-8")
169
+ existing = root / "knowledge" / "existing.md"
170
+ existing.parent.mkdir()
171
+ existing.write_text(f"<!-- xid: {xid} -->\n# Customized rules\n", encoding="utf-8")
172
+ before = snapshot(source)
173
+ result = import_legacy_assets(root, str(source), ["knowledge/rules.md"])
174
+ assert not result["ok"]
175
+ assert "XID conflict" in result["migration"]["remaining"][0]["error"]
176
+ assert XRefCatalog.build(root).get_document_by_xid(xid)["title"] == "Customized rules"
177
+ assert snapshot(source) == before
178
+
179
+
180
+ def test_unsupported_skill_resources_are_reported_without_false_completion(tmp_path):
181
+ source, root = legacy(tmp_path)
182
+ (source / "skills/nested/review/helper.py").write_text("print('legacy')", encoding="utf-8")
183
+ plan = plan_legacy_migration(root, str(source))
184
+ assert "skills/nested/review/helper.py" in plan["unsupported"]
185
+ assert not import_legacy_assets(root, str(source), ["skills/nested/review"])["ok"]
186
+ assert migration_context(root)["status"] == "in_progress"
187
+ assert not (root / "skills").exists()
188
+
189
+
190
+ def test_live_stdio_retrieval_and_restart(tmp_path):
191
+ pytest.importorskip("mcp")
192
+ from mcp.client.session import ClientSession
193
+ from mcp.client.stdio import StdioServerParameters, stdio_client
194
+ source, root = legacy(tmp_path)
195
+ original = snapshot(source)
196
+ params = StdioServerParameters(command=sys.executable,
197
+ args=["-m", "xrefkit", "mcp", "serve", "--repo", str(root)])
198
+ async def exercise():
199
+ async def call(session, tool, arguments=None):
200
+ result = await session.call_tool(tool, arguments or {})
201
+ assert not result.isError, result
202
+ return result.structuredContent or json.loads(result.content[0].text)
203
+ async with stdio_client(params) as (read, write):
204
+ async with ClientSession(read, write) as session:
205
+ await session.initialize()
206
+ startup = await call(session, "get_startup_context")
207
+ assert startup["legacy_migration"]["status"] == "not_started"
208
+ plan = await call(session, "plan_legacy_migration", {"source": str(source)})
209
+ argv = [sys.executable, *plan["client_import_command"][1:]]
210
+ for target in plan["targets"]:
211
+ argv.extend(["--target", target])
212
+ proc = await asyncio.to_thread(subprocess.run, argv, capture_output=True, text=True, check=False)
213
+ assert proc.returncode == 0, proc.stdout + proc.stderr
214
+ result = json.loads(proc.stdout)
215
+ assert result["ok"]
216
+ skill_id = result["converted_skills"][0]["skill_id"]
217
+ await call(session, "get_skill", {"skill_id": skill_id})
218
+ context = (await call(session, "get_startup_context"))["legacy_migration"]
219
+ assert context["status"] == "in_progress"
220
+ for unit in context["remaining"]:
221
+ for pending in unit["retrievals"]:
222
+ await call(session, "get_document_by_xid", {"xid": pending["xid"]})
223
+ assert (await call(session, "get_startup_context"))["legacy_migration"]["status"] == "completed"
224
+ async with stdio_client(params) as (read, write):
225
+ async with ClientSession(read, write) as session:
226
+ await session.initialize()
227
+ context = (await call(session, "get_startup_context"))["legacy_migration"]
228
+ assert context["status"] == "completed"
229
+ assert not context["client_instructions"]
230
+ asyncio.run(exercise())
231
+ assert snapshot(source) == original
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.5.1"
5
+ __version__ = "0.6.1"