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.
- {xrefkit-0.5.0/xrefkit.egg-info → xrefkit-0.6.0}/PKG-INFO +39 -1
- {xrefkit-0.5.0 → xrefkit-0.6.0}/README.md +38 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/pyproject.toml +1 -1
- xrefkit-0.6.0/tests/test_attention_pet.py +257 -0
- xrefkit-0.6.0/tests/test_attention_pet_client_protocol.py +302 -0
- xrefkit-0.6.0/tests/test_attention_pet_codex_session.py +100 -0
- xrefkit-0.6.0/tests/test_attention_pet_fit.py +396 -0
- xrefkit-0.6.0/tests/test_embedded_startup_pack.py +45 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/__init__.py +1 -1
- xrefkit-0.6.0/xrefkit/attention_pet/__init__.py +1 -0
- xrefkit-0.6.0/xrefkit/attention_pet/__main__.py +78 -0
- xrefkit-0.6.0/xrefkit/attention_pet/client.py +80 -0
- xrefkit-0.6.0/xrefkit/attention_pet/client_protocol.py +141 -0
- xrefkit-0.6.0/xrefkit/attention_pet/codex_session.py +151 -0
- xrefkit-0.6.0/xrefkit/attention_pet/evaluator.py +232 -0
- xrefkit-0.6.0/xrefkit/attention_pet/model.py +246 -0
- xrefkit-0.6.0/xrefkit/attention_pet/presentation.py +195 -0
- xrefkit-0.6.0/xrefkit/attention_pet/profiles.py +51 -0
- xrefkit-0.6.0/xrefkit/attention_pet/scenarios.py +42 -0
- xrefkit-0.6.0/xrefkit/attention_pet/server.py +165 -0
- xrefkit-0.6.0/xrefkit/attention_pet/store.py +118 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/cli.py +5 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/catalog.py +15 -1
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/startup_contract_pack.py +8 -6
- xrefkit-0.6.0/xrefkit/resources/attention_pet/client-state.schema.json +306 -0
- xrefkit-0.6.0/xrefkit/resources/attention_pet/pet.css +97 -0
- xrefkit-0.6.0/xrefkit/resources/attention_pet/pet.html +42 -0
- xrefkit-0.6.0/xrefkit/resources/attention_pet/pet.js +249 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0/xrefkit.egg-info}/PKG-INFO +39 -1
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/SOURCES.txt +21 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/LICENSE +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/setup.cfg +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_artifact_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_boundary_analysis.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_brownfield_csharp_structure_definitions.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_brownfield_file_editing_protocol.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_business_intake_conversation_definitions.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_business_intake_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_calibration_lint.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_catalog_preparation_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_check_skill_knowledge_xids.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_cli.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_code_constraint_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_collect_analyzer_sarif.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_constraint_derivation_family_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_constraint_derivation_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_contribution_returns.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_convert_to_xrefkit_skill.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_cs_scope_probe.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_csharp_commonality.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_csharp_naming_profile.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_ctx.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_cutover_readiness.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_dashboard.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_database_batch_definition_migration.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_decision_trace.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_design_business_intake_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_design_planning_definition_migration.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_dotnet_definition_candidate.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_editorial_family_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_editorial_intake_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_editorial_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_error_policy_audit.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_error_policy_locator.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_execution_binding.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_existing_skill_definition_batch.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_final_skilldefinition_batch.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_flow_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_fm_multiroot.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gate.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway_mcp.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway_skill_adapter.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_gateway_work_items.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_goal_desired_state.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_host_precheck.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_human_evaluation.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_implementation_flow_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_implementation_review_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_inbound_uploads.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_instruction_workflow.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_knowledge_relations_validator.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_language_review_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_package_routing.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_setup.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_subagent_startup.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_mcp_subagent_startup_integration.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_os_family_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_os_knowledge_family_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_os_selected_family.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_ownership.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_packmeta.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_project_quality_baseline.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_qa_report_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_resource_provider.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_runtime_contracts.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_sarif_to_locator.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_security_definition_migration.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition_governance.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition_mcp.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_definition_runtime.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_edits.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_maturity_return.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skill_runtime_audit.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skillmeta.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_skills_sync.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_structure_catalog.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_subagent_startup.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_subagent_startup_boundaries.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_wbs.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_workflow_family_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xref.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_instance.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_tools.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_v2_discovery.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_v2_models.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/tests/test_xrefkit_v2_pipeline.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/__main__.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/boundary_analysis.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/catalog_cli.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/contracts.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/ctx.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/dashboard.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/decision_trace.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/discovery.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/execution_binding.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/execution_binding_cli.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/gate.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/gateway.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/goalstate.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/hashing.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/host_precheck.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/import_skill.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/instance.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/loaders.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/__init__.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/audit.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/bootstrap.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/cli.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/client_cache.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/client_flow.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/context_registry.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/context_token.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/contracts.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/contribution_adoption.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/contribution_returns.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/dist.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/gateway.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/inbound_uploads.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/knowledge_edits.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/ownership.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/repository.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/schemas.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/server.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/setup.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/skill_edits.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/skill_maturity.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp/subagent_startup.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/mcp_tools.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/__init__.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/common.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/effective_bundle.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/human_evaluation.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/local_manifest.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/package_manifest.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/run_log.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/server_config.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/models/skill_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/operations_cli.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/ownership.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/packmeta.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/registry.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resolver.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resource_provider.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/contracts.json +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/current.json +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/7a682a5272907354/contracts.json +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/7a682a5272907354/model_body.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/9929294385ccb7b0/contracts.json +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/generations/9929294385ccb7b0/model_body.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/model_body.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/000_agent_entry.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/011_startup_xref_routing.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/015_shared_memory_operations.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/016_uncertainty_protocol.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/017_base_and_xref_layering.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/resources/base/startup_sources/053_context_direction_security_guard.md +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/runlog.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skill_definition.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skill_definition_catalog.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skill_definition_governance.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skillmeta.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skillrun.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/skills_sync.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/structure_catalog.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/subagent_startup.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/tools/__init__.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/tools/__main__.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/v2_cli.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/wbs.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/workspace.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit/xref.py +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/dependency_links.txt +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/entry_points.txt +0 -0
- {xrefkit-0.5.0 → xrefkit-0.6.0}/xrefkit.egg-info/requires.txt +0 -0
- {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.
|
|
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
|

|
|
@@ -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
|

|
|
@@ -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)
|