agent-tune-kit 2.3.5__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/.codex-plugin/plugin.json +1 -1
  2. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/PKG-INFO +3 -3
  3. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/README.en.md +2 -2
  4. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/README.md +2 -2
  5. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/__init__.py +1 -1
  6. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/projects.py +74 -18
  7. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/records.py +3 -1
  8. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/shared/base.css +3 -11
  9. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/trace/app.js +63 -42
  10. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/trace/page.html +1 -2
  11. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/trace/styles.css +8 -1
  12. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/traces/app.js +116 -62
  13. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/traces/page.html +1 -1
  14. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/traces/styles.css +13 -4
  15. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/trace.py +11 -6
  16. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/traces.py +50 -30
  17. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/trace-dashboard-design.md +16 -14
  18. agent_tune_kit-2.4.0/docs/trace-project-attribution-design.md +99 -0
  19. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/trace-tuning.md +13 -10
  20. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/pyproject.toml +1 -1
  21. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/validate_skill_pack.py +1 -1
  22. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-trace-detect/SKILL.md +5 -3
  23. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-trace-diagnose/SKILL.md +13 -11
  24. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-trace-init/SKILL.md +4 -2
  25. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-trace-optimize/SKILL.md +5 -3
  26. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-trace-verify/SKILL.md +4 -2
  27. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-visualize-trace/SKILL.md +4 -2
  28. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_install_plugin.py +1 -1
  29. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_release_scripts.py +1 -1
  30. agent_tune_kit-2.4.0/tests/test_trace_projects.py +147 -0
  31. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_visualize_trace.py +3 -1
  32. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_visualize_traces.py +32 -6
  33. agent_tune_kit-2.4.0/tests/trace-controls.cjs +54 -0
  34. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-dashboard.cjs +134 -58
  35. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-issue-cards.cjs +4 -4
  36. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-projects.cjs +81 -71
  37. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-stage-cards.cjs +10 -0
  38. agent_tune_kit-2.3.5/agent_tune_kit/visualize/assets/shared/project-menu.js +0 -32
  39. agent_tune_kit-2.3.5/docs/trace-project-attribution-design.md +0 -246
  40. agent_tune_kit-2.3.5/tests/test_trace_projects.py +0 -65
  41. agent_tune_kit-2.3.5/tests/trace-controls.cjs +0 -86
  42. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/.gitignore +0 -0
  43. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/__init__.py +0 -0
  44. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/__main__.py +0 -0
  45. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/dataset.py +0 -0
  46. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/evaluation.py +0 -0
  47. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/main.py +0 -0
  48. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/output.py +0 -0
  49. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/results.py +0 -0
  50. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/trace.py +0 -0
  51. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/cli/visualize.py +0 -0
  52. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/datasets/__init__.py +0 -0
  53. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/datasets/prepare.py +0 -0
  54. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/datasets/truth.py +0 -0
  55. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/datasets/updates.py +0 -0
  56. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/datasets/validation.py +0 -0
  57. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/errors.py +0 -0
  58. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/__init__.py +0 -0
  59. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/calibration.py +0 -0
  60. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/logs.py +0 -0
  61. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/rules.py +0 -0
  62. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/runner.py +0 -0
  63. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/runtime.py +0 -0
  64. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/execution/tracing.py +0 -0
  65. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/installer.py +0 -0
  66. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/results/__init__.py +0 -0
  67. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/results/assessment.py +0 -0
  68. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/results/evidence.py +0 -0
  69. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/results/judgments.py +0 -0
  70. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/results/summary.py +0 -0
  71. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/results/versions.py +0 -0
  72. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/__init__.py +0 -0
  73. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/archive.py +0 -0
  74. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/capture.py +0 -0
  75. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/categories.py +0 -0
  76. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/langfuse.py +0 -0
  77. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/migrate.py +0 -0
  78. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/progress.py +0 -0
  79. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/query.py +0 -0
  80. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/segments.py +0 -0
  81. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/traces/store.py +0 -0
  82. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/utils/__init__.py +0 -0
  83. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/utils/csv_io.py +0 -0
  84. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/utils/files.py +0 -0
  85. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/__init__.py +0 -0
  86. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/dataset/app.js +0 -0
  87. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/dataset/page.html +0 -0
  88. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/dataset/styles.css +0 -0
  89. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/dataset/vendor/echarts.min.js +0 -0
  90. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/failures/app.js +0 -0
  91. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/failures/page.html +0 -0
  92. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/failures/styles.css +0 -0
  93. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/trace/lanes.js +0 -0
  94. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/trace/vendor/markdown-it.LICENSE +0 -0
  95. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/assets/trace/vendor/markdown-it.min.js +0 -0
  96. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/dataset.py +0 -0
  97. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/failures.py +0 -0
  98. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/fields.py +0 -0
  99. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/history.py +0 -0
  100. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/render.py +0 -0
  101. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/visualize/timeline.py +0 -0
  102. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/agent_tune_kit/workspace.py +0 -0
  103. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/assets/agent_tune_kit_evaluation.png +0 -0
  104. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/assets/agent_tune_kit_trace.png +0 -0
  105. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/assets/arch-en.png +0 -0
  106. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/assets/arch.png +0 -0
  107. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/assets/architecture.en.svg +0 -0
  108. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/assets/architecture.svg +0 -0
  109. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/ato-atk-project-boundary-recommendation-2026-11-05.md +0 -0
  110. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/cli-skill-modularization-plan.md +0 -0
  111. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/codex_agent_tuning_prd.md +0 -0
  112. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/execution-evidence-and-tracing-plan.md +0 -0
  113. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/ground-truth-and-grading-assessment-2026-10-25.md +0 -0
  114. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/local-context.md +0 -0
  115. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/shared-versioning-and-confirmation.md +0 -0
  116. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/skill-template-pack-usage.md +0 -0
  117. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/surginame-atk-capability-review-2026-10-27.md +0 -0
  118. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/trace-change-verification-readability-diagnosis.md +0 -0
  119. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/trace-swimlane-design.md +0 -0
  120. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/trace-tuning-validation.md +0 -0
  121. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/docs/truth-and-assessment.md +0 -0
  122. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/check-release.py +0 -0
  123. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/dev-refresh-install.sh +0 -0
  124. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/install_plugin.py +0 -0
  125. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/publish-pypi.sh +0 -0
  126. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/publish-release.py +0 -0
  127. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/release-version.py +0 -0
  128. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/scripts/release-version.sh +0 -0
  129. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-dataset/SKILL.md +0 -0
  130. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-dataset/scripts/update_dataset.py +0 -0
  131. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-diagnose/SKILL.md +0 -0
  132. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-eval/SKILL.md +0 -0
  133. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-init/SKILL.md +0 -0
  134. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-optimize/SKILL.md +0 -0
  135. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-report/SKILL.md +0 -0
  136. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-trace-capture/SKILL.md +0 -0
  137. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-visualize-dataset/SKILL.md +0 -0
  138. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-visualize-dataset/scripts/generate_dataset_browser.py +0 -0
  139. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-visualize-failures/SKILL.md +0 -0
  140. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/skills/atk-visualize-failures/scripts/generate_failure_browser.py +0 -0
  141. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/templates/.atk/runner/eval_runner.py.md +0 -0
  142. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/templates/.atk/runner/failure_rule.py.md +0 -0
  143. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/templates/.atk/runner/import_evidence.py.md +0 -0
  144. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/templates/.gitkeep +0 -0
  145. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/__init__.py +0 -0
  146. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/conftest.py +0 -0
  147. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/fixtures/legacy_eval_runner.py.txt +0 -0
  148. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/fixtures/managed_v1.py.md +0 -0
  149. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_assessment_contract.py +0 -0
  150. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_atk_init_contract.py +0 -0
  151. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_cli_workflow.py +0 -0
  152. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_dataset_updates.py +0 -0
  153. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_diagnosis.py +0 -0
  154. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_evidence_import.py +0 -0
  155. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_evidence_judgments.py +0 -0
  156. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_evidence_packaging.py +0 -0
  157. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_evidence_versions.py +0 -0
  158. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_execution_evidence.py +0 -0
  159. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_execution_outcomes.py +0 -0
  160. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_execution_tracing.py +0 -0
  161. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_generate_dataset_browser.py +0 -0
  162. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_generate_failure_browser.py +0 -0
  163. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_judge_calibration.py +0 -0
  164. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_local_context.py +0 -0
  165. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_plugin_cache.py +0 -0
  166. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_runner_output_columns.py +0 -0
  167. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_runner_template_row_logging.py +0 -0
  168. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_runtime_contracts.py +0 -0
  169. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_snapshot_reads.py +0 -0
  170. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_capture.py +0 -0
  171. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_categories.py +0 -0
  172. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_cli.py +0 -0
  173. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_event_list.py +0 -0
  174. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_langfuse.py +0 -0
  175. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_progress.py +0 -0
  176. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_query.py +0 -0
  177. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_records.py +0 -0
  178. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_scroll_layout.py +0 -0
  179. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_segments.py +0 -0
  180. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_trace_store.py +0 -0
  181. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_truth_browser.py +0 -0
  182. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_truth_cli.py +0 -0
  183. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_truth_contract.py +0 -0
  184. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_truth_runner.py +0 -0
  185. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/test_v2_progress.py +0 -0
  186. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-event-detail.cjs +0 -0
  187. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-event-layout.cjs +0 -0
  188. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-event-list.cjs +0 -0
  189. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-event-wheel.cjs +0 -0
  190. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-fragments.cjs +0 -0
  191. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-preview-scroll.cjs +0 -0
  192. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-scroll-layout.cjs +0 -0
  193. {agent_tune_kit-2.3.5 → agent_tune_kit-2.4.0}/tests/trace-selection-reason.cjs +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tune-kit",
3
- "version": "2.3.5",
3
+ "version": "2.4.0",
4
4
  "description": "Local Codex plugin for batch Agent evaluation and session trace tuning with evidence-backed issues, focused changes, verification and offline review.",
5
5
  "author": {
6
6
  "name": "hustyichi",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-tune-kit
3
- Version: 2.3.5
3
+ Version: 2.4.0
4
4
  Summary: Local Codex plugin for iterative Agent tuning with guided Skills, reusable runner templates, versioned results, and static validation.
5
5
  Project-URL: Homepage, https://github.com/hustyichi/agent-tune-kit
6
6
  Project-URL: Repository, https://github.com/hustyichi/agent-tune-kit
@@ -196,6 +196,6 @@ Each session archive is stored under `.atk/traces/<session_id>/` in your Agent p
196
196
 
197
197
  [Trace tuning guide for source configuration, supported formats, and archive layout](docs/trace-tuning.md)
198
198
 
199
- [Multi-project attribution and filtering proposal (not yet implemented, Chinese)](docs/trace-project-attribution-design.md)
199
+ [Multi-project attribution and filtering details (Chinese)](docs/trace-project-attribution-design.md)
200
200
 
201
- Use `atk trace project` to register workspace projects and submodules. Issues can have multiple root-cause projects, filterable in the dashboard and session issue list. Repair details describe modification locations.
201
+ Use `atk trace project` to register workspace projects and submodules. Record associated projects with reasonable causal explanations for human review, and filter them in the dashboard and session issue list. Associations do not require a completed or verified fix; reasons describe uncertainty, and repair details describe modification locations.
@@ -182,6 +182,6 @@ Each session archive is stored under `.atk/traces/<session_id>/` in your Agent p
182
182
 
183
183
  [Trace tuning guide for source configuration, supported formats, and archive layout](docs/trace-tuning.md)
184
184
 
185
- [Multi-project attribution and filtering proposal (not yet implemented, Chinese)](docs/trace-project-attribution-design.md)
185
+ [Multi-project attribution and filtering details (Chinese)](docs/trace-project-attribution-design.md)
186
186
 
187
- Use `atk trace project` to register workspace projects and submodules. Issues can have multiple root-cause projects, filterable in the dashboard and session issue list. Repair details describe modification locations.
187
+ Use `atk trace project` to register workspace projects and submodules. Record associated projects with reasonable causal explanations for human review, and filter them in the dashboard and session issue list. Associations do not require a completed or verified fix; reasons describe uncertainty, and repair details describe modification locations.
@@ -182,6 +182,6 @@ $atk-trace-verify 验证 <session-id> 的 ISSUE-001 对应候选,并检查相
182
182
 
183
183
  [轨迹调优指南,包含来源配置、支持格式和归档结构](docs/trace-tuning.md)
184
184
 
185
- [多工程关联与项目筛选方案(待实现)](docs/trace-project-attribution-design.md)
185
+ [多工程关联与项目筛选说明](docs/trace-project-attribution-design.md)
186
186
 
187
- 支持通过 `atk trace project` 登记工作区及子模块;诊断可记录多个根因项目,大盘与会话问题栏按根因项目筛选。修改位置在修复详情中说明。
187
+ 支持通过 `atk trace project` 登记工作区及子模块;诊断按合理的原因解释记录“关联项目”,供人工审核,大盘与会话问题栏均可筛选。关联不以修复完成或验证通过为前提;不确定性写在关联理由中,修改位置在修复详情中说明。
@@ -1,3 +1,3 @@
1
1
  """Agent Tune Kit package."""
2
2
 
3
- __version__ = "2.3.5"
3
+ __version__ = "2.4.0"
@@ -1,4 +1,4 @@
1
- """Workspace project registry and evidence-backed attribution."""
1
+ """Workspace project registry and issue associations for human review."""
2
2
 
3
3
  import configparser
4
4
  import hashlib
@@ -150,14 +150,12 @@ def validate_links(session_dir, links):
150
150
  projects = project_map(Path(session_dir).parent)
151
151
  seen = set()
152
152
  for link in links:
153
- if not isinstance(link, dict):
153
+ if not isinstance(link, dict) or set(link) - {"project_id", "reason", "evidence", "source_paths"}:
154
154
  raise UserActionRequired("Invalid project link.")
155
- key = (link.get("project_id"), link.get("role"))
156
- if not all(isinstance(k, str) for k in key) or key[0] not in projects or key in seen:
155
+ key = link.get("project_id")
156
+ if not isinstance(key, str) or key not in projects or key in seen:
157
157
  raise UserActionRequired("Unknown or duplicate project link.")
158
158
  seen.add(key)
159
- if key[1] not in {"root_cause", "improvement"} or link.get("evidence_state") not in {"hypothesis", "supported"}:
160
- raise UserActionRequired("Invalid project attribution state.")
161
159
  if (
162
160
  not isinstance(link.get("reason"), str)
163
161
  or not link["reason"].strip()
@@ -176,26 +174,84 @@ def validate_links(session_dir, links):
176
174
  return links
177
175
 
178
176
 
179
- def root_projects(root, issue):
180
- projects = project_map(root)
177
+ def _display_name(entry, project_id):
178
+ return entry.get("abbreviation") or entry.get("name") or project_id
179
+
180
+
181
+ def linked_projects(root, issue, projects=None):
182
+ """Return the display identity of each project an issue links to, once per project.
183
+
184
+ Pages only need the stable ID, the shown name and aliases for search; the rest of the registry stays out of them.
185
+ Pass ``projects`` (from ``project_map``) when resolving many issues so the registry is read once.
186
+ """
187
+ projects = project_map(root) if projects is None else projects
181
188
  result = []
182
189
  links = issue.get("project_links", [])
183
190
  if not isinstance(links, list):
184
191
  return result
192
+ seen = set()
185
193
  for link in links:
186
- if (
187
- not isinstance(link, dict)
188
- or not isinstance(link.get("project_id"), str)
189
- or link.get("role") != "root_cause"
190
- ):
194
+ if not isinstance(link, dict) or not isinstance(link.get("project_id"), str) or link["project_id"] in seen:
191
195
  continue
192
- entry = projects.get(link["project_id"], {})
196
+ project_id = link["project_id"]
197
+ seen.add(project_id)
198
+ entry = projects.get(project_id, {})
199
+ aliases = entry.get("aliases")
200
+ result.append(
201
+ {
202
+ "id": project_id,
203
+ "name": _display_name(entry, project_id),
204
+ "aliases": [alias for alias in aliases if isinstance(alias, str)] if isinstance(aliases, list) else [],
205
+ }
206
+ )
207
+ return result
208
+
209
+
210
+ def project_index(root, linked, projects=None):
211
+ """Summarize the projects that issues actually link to, for the pages' single-choice project filter.
212
+
213
+ ``linked`` yields ``(session_key, linked_projects)`` per issue. Registered projects without any linked issue are
214
+ left out: nothing could be found by choosing them. A linked ID missing from the registry stays in, shown by its ID,
215
+ so its issues remain reachable. Counts are per project; an issue linked to several projects counts once in each,
216
+ so the entries must not be added up as an issue total.
217
+ """
218
+ projects = project_map(root) if projects is None else projects
219
+ found = {}
220
+ for session, items in linked:
221
+ for item in items:
222
+ entry = found.setdefault(
223
+ item["id"], {"id": item["id"], "name": item["name"], "issues": 0, "sessions": set()}
224
+ )
225
+ entry["issues"] += 1
226
+ entry["sessions"].add(session)
227
+ names = {}
228
+ for entry in found.values():
229
+ names.setdefault(entry["name"], []).append(entry["id"])
230
+ result = []
231
+ for entry in found.values():
232
+ registered = projects.get(entry["id"])
233
+ name = entry["name"]
234
+ if len(names[name]) > 1:
235
+ # Two projects share an abbreviation: tell the filter's choices apart by where each one lives.
236
+ tail = Path(registered["path"]).name if registered and registered.get("path") else entry["id"]
237
+ name = f"{name} · {tail or entry['id']}"
193
238
  result.append(
194
239
  {
195
- **entry,
196
- "id": link["project_id"],
197
- "name": entry.get("abbreviation") or entry.get("name") or link["project_id"],
198
- "pending": link.get("evidence_state") != "supported",
240
+ "id": entry["id"],
241
+ "name": name,
242
+ "issues": entry["issues"],
243
+ "sessions": len(entry["sessions"]),
244
+ "registered": registered is not None,
245
+ "deprecated": bool(registered and registered.get("deprecated") is True),
199
246
  }
200
247
  )
248
+ result.sort(
249
+ key=lambda item: (
250
+ item["deprecated"],
251
+ not item["registered"],
252
+ -item["issues"],
253
+ item["name"].casefold(),
254
+ item["id"],
255
+ )
256
+ )
201
257
  return result
@@ -172,6 +172,7 @@ def prepare_issue(session_dir, data, *, dry_run=False):
172
172
  "created_at": now(),
173
173
  "status": "open",
174
174
  "segments": [],
175
+ "project_links": [],
175
176
  }
176
177
  )
177
178
  if "project_links" in data:
@@ -347,9 +348,10 @@ def save_diagnosis(session_dir, issue_id, text, diagnosis_id=None, expected_revi
347
348
  "description": issue["description"],
348
349
  "evidence": issue["evidence"],
349
350
  "segments": issue.get("segments", []),
351
+ "project_links": validate_links(directory, issue.get("project_links", [])),
350
352
  "created_at": now(),
351
353
  }
352
- for optional in ("selection_reason", "category", "project_links"):
354
+ for optional in ("selection_reason", "category"):
353
355
  if optional in issue:
354
356
  snapshot[optional] = issue[optional]
355
357
  if issue.get("project_links"):
@@ -63,15 +63,15 @@ h3 { font-size: 14px }
63
63
  .progress-dot.excluded { background: transparent; box-shadow: inset 0 0 0 1.5px var(--p-pending) }
64
64
  .category-tag { display: inline-block; max-width: 100%; background: #fff8f1; color: var(--accent); font-size: 11px; line-height: 1.5; padding: 2px 6px; border-radius: 4px; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
65
65
  a.category-tag:hover { background: #ffe8d2 }
66
- input:not([type=checkbox]), select, .project-choice > summary {
66
+ input:not([type=checkbox]), select {
67
67
  display: block; height: var(--control-height); min-width: 0; max-width: 100%; padding: 0 11px;
68
68
  font-size: 13px; line-height: normal; border-color: #cdd8e1;
69
69
  }
70
- select, .project-choice > summary {
70
+ select {
71
71
  appearance: none; padding-right: 32px; text-overflow: ellipsis;
72
72
  background: white url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e") no-repeat right 11px center / 14px;
73
73
  }
74
- input:not([type=checkbox]):hover, select:hover, .project-choice > summary:hover { border-color: #9baebb }
74
+ input:not([type=checkbox]):hover, select:hover { border-color: #9baebb }
75
75
  input[type=checkbox] { width: 16px; height: 16px; margin: 0; padding: 0; flex-shrink: 0; accent-color: var(--brand) }
76
76
  @media (forced-colors: active) {
77
77
  select { appearance: auto; background-image: none; padding-right: 8px }
@@ -96,11 +96,3 @@ input[type=checkbox] { width: 16px; height: 16px; margin: 0; padding: 0; flex-sh
96
96
  .drawer-resizer { display: none }
97
97
  }
98
98
 
99
- /* Project attribution belongs to issues; the menu is shared by both issue views. */
100
- .project-choice { position: relative; min-width: 0; max-width: 100%; margin-bottom: 7px; font-size: 13px }
101
- .project-choice > summary { line-height: calc(var(--control-height) - 2px); list-style: none; white-space: nowrap; overflow: hidden; border: 1px solid #cdd8e1; border-radius: 6px }
102
- .project-choice > summary::-webkit-details-marker { display: none }
103
- .project-choice[open] > summary { border-color: #deb493; color: var(--accent) }
104
- .project-menu { position: fixed; z-index: 20; width: min(300px, calc(100vw - 16px)); max-height: min(320px, calc(100dvh - 16px)); overflow: auto; overscroll-behavior: contain; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: white; box-shadow: 0 6px 20px #0002 }
105
- .project-menu p { margin: 0 0 8px }
106
- #project-filter label { display: flex; align-items: center; gap: 8px; padding: 6px 0; overflow-wrap: anywhere }
@@ -23,26 +23,14 @@
23
23
  const captureById = new Map(data.captures.map(c => [c.id, c]));
24
24
  const issueById = new Map(data.issues.map(item => [item.record.id, item]));
25
25
 
26
- const projectSelection = new Set(new URLSearchParams(location.hash.slice(1)).getAll("project"));
27
- const projectMatches = item => !projectSelection.size || [...projectSelection].some(id => {
28
- const roots = item.root_projects || [];
29
- if (id === "~unattributed") return !roots.length;
30
- return roots.some(root => root.id === id);
31
- });
32
- const projectInputs = [];
33
- for (const p of [{id: "~unattributed", name: "待归因"}, ...Object.values(data.projects || {})]) {
34
- const label = el("label"), input = el("input");
35
- input.type = "checkbox"; input.value = p.id;
36
- input.checked = projectSelection.has(p.id);
37
- label.append(input, document.createTextNode(p.abbreviation || p.name));
38
- $("project-filter").append(label); projectInputs.push(input);
39
- input.onchange = () => $("project-filter").onchange();
40
- }
41
- const updateProjectSummary = () => {
42
- $("project-summary").textContent = projectSelection.size ? `关联项目:已选 ${projectSelection.size} 项` : "关联项目:全部";
43
- };
44
- updateProjectSummary();
45
- $("project-clear").onclick = () => { projectInputs.forEach(input => { input.checked = false; }); $("project-filter").onchange(); };
26
+ // One linked project at a time, or every issue. Only projects this session's issues link to are offered.
27
+ const PROJECTS = data.project_index || [];
28
+ const PROJECT = new Map(PROJECTS.map(entry => [entry.id, entry]));
29
+ // Older links may carry several projects or the removed empty-association value: keep the first one still offered.
30
+ const projectFrom = params => params.getAll("project").find(id => PROJECT.has(id)) || "";
31
+ let railProject = projectFrom(new URLSearchParams(location.hash.slice(1)));
32
+ const projectMatches = item => !railProject || (item.linked_projects || []).some(project => project.id === railProject);
33
+ const projectLabel = entry => `${entry.name}${entry.deprecated ? "(已弃用)" : entry.registered === false ? "(未登记)" : ""}`;
46
34
 
47
35
  const filters = ["trace-filter", "type-filter", "trace-search", "record-mode"];
48
36
  const list = $("trace-list"), scroller = $("trace-scroll");
@@ -69,11 +57,11 @@
69
57
  const dashboardHash = params => `../index.html#${params}`;
70
58
  const projectLabels = item => {
71
59
  const row = el("p", "关联项目:", "issue-projects");
72
- const projects = item.root_projects || [];
73
- if (!projects.length) row.append(document.createTextNode("待归因"));
60
+ const projects = item.linked_projects || [];
61
+ if (!projects.length) row.append(document.createTextNode("无"));
74
62
  projects.forEach((project, index) => {
75
63
  if (index) row.append(document.createTextNode("、"));
76
- const name = project.name + (project.pending ? "(待确认)" : "");
64
+ const name = project.name;
77
65
  const label = el(data.dashboard.available ? "a" : "span", name);
78
66
  if (data.dashboard.available) {
79
67
  label.href = dashboardHash(`view=category&project=${encodeURIComponent(project.id)}`);
@@ -108,7 +96,6 @@
108
96
  onRoot: index => openEventDrawer(currentCapture.nodes[index], document.activeElement),
109
97
  onIssueBadge: track => {
110
98
  $("issue-search").value = "";
111
- $("issue-status").value = "";
112
99
  railFilter = {track, ids: data.issues.filter(item => item.fragments.some(part => part.capture_id === currentCapture?.id && part.track === track)).map(item => item.record.id)};
113
100
  renderIssueRail();
114
101
  },
@@ -321,6 +308,16 @@
321
308
  const headings = headingNodes.map(node => node.textContent);
322
309
  const structured = headings.join("|") === "问题描述|问题根因|最小修复方案"
323
310
  || headings.join("|") === "Issue description|Root cause|Minimal fix";
311
+ if (snapshot?.project_links) {
312
+ const entries = new Map((snapshot.project_registry?.projects || []).map(p => [p.id, p]));
313
+ const associations = el("div");
314
+ associations.append(el("p", "诊断时的关联项目:" + (snapshot.project_links.length ? "" : "无")));
315
+ for (const link of snapshot.project_links) {
316
+ const entry = entries.get(link.project_id);
317
+ associations.append(el("p", `${entry?.abbreviation || entry?.name || link.project_id}:${link.reason}`));
318
+ }
319
+ body.prepend(associations);
320
+ }
324
321
  if (index === 0 && structured) {
325
322
  container.append(body);
326
323
  continue;
@@ -335,11 +332,6 @@
335
332
  container.append(description);
336
333
  }
337
334
  const details = el("details", undefined, "detail-section");
338
- if (snapshot?.project_links) {
339
- const entries = new Map((snapshot.project_registry?.projects || []).map(p => [p.id, p]));
340
- const names = snapshot.project_links.filter(p => p.role === "root_cause").map(p => { const entry = entries.get(p.project_id); return (entry?.abbreviation || entry?.name || p.project_id) + (p.evidence_state === "supported" ? "" : "(待确认)"); });
341
- body.prepend(el("p", `诊断时的关联项目:${names.join("、") || "待归因"}`));
342
- }
343
335
  const name = diagnosis.reference.path.split("/").pop();
344
336
  details.append(el("summary", `展开原诊断 · ${name}`), body);
345
337
  container.append(details);
@@ -1116,16 +1108,51 @@
1116
1108
  if (mine && investigation.index >= 2) card.querySelectorAll(".seg-more").forEach(more => { more.open = true; });
1117
1109
  }
1118
1110
  }
1111
+ const searchMatches = (item, text) => !text || pretty(item.record).toLocaleLowerCase().includes(text) ||
1112
+ progressOf(item).label.toLocaleLowerCase().includes(text) || (item.linked_projects || []).some(p => [p.name, ...(p.aliases || [])].some(name => name.toLocaleLowerCase().includes(text)));
1113
+ function chooseProject(id) {
1114
+ railProject = railProject === id ? "" : id;
1115
+ syncHash(); renderIssueRail();
1116
+ }
1117
+ // Project counts follow the search only: the lane filter and the chosen project leave every project's real size.
1118
+ function renderProjectChoice(text) {
1119
+ const box = $("project-legend");
1120
+ box.replaceChildren();
1121
+ const issues = data.issues.filter(item => searchMatches(item, text));
1122
+ const counts = new Map();
1123
+ for (const item of issues) for (const project of item.linked_projects || []) {
1124
+ if (PROJECT.has(project.id)) counts.set(project.id, (counts.get(project.id) || 0) + 1);
1125
+ }
1126
+ const order = new Map(PROJECTS.map((entry, index) => [entry.id, index]));
1127
+ const shown = PROJECTS.filter(entry => counts.get(entry.id) || entry.id === railProject).sort((a, b) =>
1128
+ Number(Boolean(a.deprecated)) - Number(Boolean(b.deprecated)) || Number(a.registered === false) - Number(b.registered === false) ||
1129
+ (counts.get(b.id) || 0) - (counts.get(a.id) || 0) || order.get(a.id) - order.get(b.id));
1130
+ // A single project that holds every issue filters nothing, so the row would only repeat the total. This is judged on
1131
+ // the whole session, not the search, so the row does not vanish while typing.
1132
+ box.hidden = !PROJECTS.length || (PROJECTS.length === 1 && !railProject && PROJECTS[0].issues === data.issues.length);
1133
+ if (box.hidden) return;
1134
+ const choice = (name, n, id, title) => {
1135
+ const item = button("", () => chooseProject(id), "project-item");
1136
+ item.setAttribute("role", "radio");
1137
+ item.setAttribute("aria-checked", String(railProject === id));
1138
+ item.title = title;
1139
+ item.append(el("span", name, "project-name"), el("span", String(n), "project-count"));
1140
+ if (!n) item.classList.add("is-empty");
1141
+ return item;
1142
+ };
1143
+ box.append(choice("全部", issues.length, "", "显示全部问题"));
1144
+ for (const entry of shown) {
1145
+ box.append(choice(projectLabel(entry), counts.get(entry.id) || 0, entry.id,
1146
+ railProject === entry.id ? `取消筛选 ${projectLabel(entry)}` : `只看关联 ${projectLabel(entry)} 的问题`));
1147
+ }
1148
+ }
1119
1149
  function renderIssueRail() {
1120
1150
  const text = $("issue-search").value.toLocaleLowerCase();
1121
- const status = $("issue-status").value;
1151
+ renderProjectChoice(text);
1122
1152
  const filtered = data.issues.filter(item => {
1123
1153
  if (!projectMatches(item) && new URLSearchParams(location.hash.slice(1)).get("issue") !== item.record.id) return false;
1124
1154
  if (railFilter && !railFilter.ids.includes(item.record.id)) return false;
1125
- const progress = progressOf(item).label;
1126
- if (status && progress !== status) return false;
1127
- if (!text) return true;
1128
- return pretty(item.record).toLocaleLowerCase().includes(text) || progress.toLocaleLowerCase().includes(text);
1155
+ return searchMatches(item, text);
1129
1156
  });
1130
1157
  $("rail-count").textContent = `${filtered.length}/${data.issues.length}`;
1131
1158
  const box = $("issue-rail-list"); box.replaceChildren();
@@ -1206,7 +1233,7 @@
1206
1233
  syncRailSelection();
1207
1234
  }
1208
1235
  function syncHash() {
1209
- const parts = [...projectSelection].map(id => `project=${encodeURIComponent(id)}`);
1236
+ const parts = railProject ? [`project=${encodeURIComponent(railProject)}`] : [];
1210
1237
  if (investigation) {
1211
1238
  parts.push(`issue=${encodeURIComponent(investigation.item.record.id)}`);
1212
1239
  const part = partNow();
@@ -1223,9 +1250,7 @@
1223
1250
  if (hash !== location.hash) history.replaceState(null, "", hash || location.pathname + location.search);
1224
1251
  const raw = hash.slice(1);
1225
1252
  const params = new URLSearchParams(raw.includes("=") ? raw : `issue=${raw}`);
1226
- projectSelection.clear(); params.getAll("project").forEach(id => projectSelection.add(id));
1227
- for (const input of projectInputs) input.checked = projectSelection.has(input.value);
1228
- updateProjectSummary();
1253
+ railProject = projectFrom(params);
1229
1254
  const issueId = params.get("issue");
1230
1255
  const seg = params.get("seg");
1231
1256
  const event = params.get("event");
@@ -1262,14 +1287,10 @@
1262
1287
  $("capture-filter").closest("label").hidden = Boolean(currentCapture?.current);
1263
1288
  options($("capture-filter"), data.captures.map(c => c.id));
1264
1289
  if (currentCapture) $("capture-filter").value = currentCapture.id;
1265
- $("project-filter").onchange = () => { projectSelection.clear(); for (const input of projectInputs) if (input.checked) projectSelection.add(input.value); updateProjectSummary(); const params = new URLSearchParams(location.hash.slice(1)); params.delete("project"); for (const id of projectSelection) params.append("project", id); history.replaceState(null, "", `#${params}`); renderIssueRail(); };
1266
1290
  $("capture-filter").onchange = () => { investigation = null; scopeBefore = null; railFilter = null; chooseCapture(); renderIssueRail(); renderTraces(); };
1267
1291
  $("trace-filter").onchange = () => focusTrace(scope());
1268
1292
  for (const id of ["trace-search","type-filter","record-mode"]) $(id).addEventListener(id === "trace-search" ? "input" : "change", () => renderTraces());
1269
- const presentProgress = new Set(data.issues.map(item => item.progress.label));
1270
- options($("issue-status"), data.progress_legend.map(entry => entry.label).filter(label => presentProgress.has(label)), "全部进度");
1271
1293
  $("issue-search").oninput = () => { railFilter = null; renderIssueRail(); };
1272
- $("issue-status").onchange = () => { railFilter = null; renderIssueRail(); };
1273
1294
  const collapsed = localStorage.getItem(RAIL_KEY) === "1";
1274
1295
  document.body.classList.toggle("rail-collapsed", collapsed);
1275
1296
  $("rail-toggle").setAttribute("aria-expanded", String(!collapsed));
@@ -21,8 +21,7 @@
21
21
  </div>
22
22
  <div class="rail-filters">
23
23
  <label class="search-label"><span class="sr-only">搜索问题</span><input id="issue-search" type="search" placeholder="搜索问题"></label>
24
- <details id="project-choice" class="project-choice"><summary id="project-summary">关联项目:全部</summary><div class="project-menu"><p>按问题实际关联项目筛选</p><button id="project-clear" type="button">清空筛选</button><div id="project-filter" role="group" aria-label="关联项目"></div></div></details>
25
- <label><span class="sr-only">状态</span><select id="issue-status"></select></label>
24
+ <div id="project-legend" class="rail-projects" role="radiogroup" aria-label="关联项目" hidden></div>
26
25
  </div>
27
26
  <div id="issue-rail-list" class="rail-list" tabindex="0" aria-label="问题列表"></div>
28
27
  </aside>
@@ -33,7 +33,14 @@ body.rail-collapsed #rail-toggle { transform: rotate(180deg) }
33
33
  .rail-filters .search-label { flex: 1 0 100%; min-width: 0 }
34
34
  .rail-filters > label { min-width: 0; max-width: 100% }
35
35
  .rail-filters input[type=search] { width: 100% }
36
- .rail-filters .project-choice { margin-bottom: 0 }
36
+ /* Project choice: one linked project or all issues; each count is the issues linked to that project. */
37
+ .rail-projects { display: flex; flex-wrap: wrap; gap: 4px; flex: 1 0 100%; min-width: 0 }
38
+ .project-item { display: inline-flex; align-items: center; gap: 5px; max-width: 100%; min-width: 0; padding: 2px 8px; border: 1px solid var(--line); border-radius: 6px; background: white; color: var(--muted); font-size: 12px; line-height: 1.5 }
39
+ .project-item:hover { background: var(--tint) }
40
+ .project-item[aria-checked=true] { background: var(--tint); color: var(--accent); border-color: #eabda0 }
41
+ .project-item .project-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
42
+ .project-item .project-count { font-weight: 700 }
43
+ .project-item.is-empty { opacity: .6 }
37
44
  .rail-list { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; scrollbar-width: thin; padding: 12px; display: flex; flex-direction: column; gap: 12px }
38
45
  .issue-card { display: flex; flex: none; flex-direction: column; gap: 10px; width: 100%; min-width: 0; text-align: left; padding: 16px; border-radius: 12px }
39
46
  .issue-card:hover, .issue-card[aria-current=true] { background: var(--paper) }