agent-tune-kit 2.3.3__tar.gz → 2.3.4__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 (179) hide show
  1. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/.codex-plugin/plugin.json +1 -1
  2. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/PKG-INFO +1 -1
  3. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/__init__.py +1 -1
  4. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/shared/base.css +9 -0
  5. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/trace/app.js +24 -9
  6. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/trace/page.html +1 -1
  7. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/trace/styles.css +3 -0
  8. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/traces/app.js +55 -46
  9. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/traces/page.html +5 -2
  10. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/traces/styles.css +22 -3
  11. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/traces.py +4 -29
  12. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/trace-dashboard-design.md +17 -7
  13. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/trace-project-attribution-design.md +9 -7
  14. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/trace-tuning.md +4 -0
  15. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/pyproject.toml +1 -1
  16. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/validate_skill_pack.py +1 -1
  17. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-trace-detect/SKILL.md +2 -0
  18. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-trace-diagnose/SKILL.md +2 -0
  19. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-trace-init/SKILL.md +2 -0
  20. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-trace-optimize/SKILL.md +2 -0
  21. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-trace-verify/SKILL.md +2 -0
  22. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-visualize-trace/SKILL.md +6 -4
  23. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_install_plugin.py +1 -1
  24. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_release_scripts.py +1 -1
  25. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_projects.py +9 -1
  26. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_visualize_traces.py +8 -8
  27. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/trace-dashboard.cjs +81 -8
  28. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/.gitignore +0 -0
  29. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/README.en.md +0 -0
  30. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/README.md +0 -0
  31. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/__init__.py +0 -0
  32. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/__main__.py +0 -0
  33. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/dataset.py +0 -0
  34. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/evaluation.py +0 -0
  35. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/main.py +0 -0
  36. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/output.py +0 -0
  37. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/results.py +0 -0
  38. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/trace.py +0 -0
  39. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/cli/visualize.py +0 -0
  40. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/datasets/__init__.py +0 -0
  41. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/datasets/prepare.py +0 -0
  42. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/datasets/truth.py +0 -0
  43. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/datasets/updates.py +0 -0
  44. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/datasets/validation.py +0 -0
  45. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/errors.py +0 -0
  46. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/__init__.py +0 -0
  47. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/calibration.py +0 -0
  48. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/logs.py +0 -0
  49. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/rules.py +0 -0
  50. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/runner.py +0 -0
  51. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/runtime.py +0 -0
  52. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/execution/tracing.py +0 -0
  53. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/installer.py +0 -0
  54. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/results/__init__.py +0 -0
  55. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/results/assessment.py +0 -0
  56. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/results/evidence.py +0 -0
  57. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/results/judgments.py +0 -0
  58. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/results/summary.py +0 -0
  59. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/results/versions.py +0 -0
  60. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/__init__.py +0 -0
  61. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/archive.py +0 -0
  62. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/capture.py +0 -0
  63. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/categories.py +0 -0
  64. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/langfuse.py +0 -0
  65. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/migrate.py +0 -0
  66. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/progress.py +0 -0
  67. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/projects.py +0 -0
  68. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/query.py +0 -0
  69. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/records.py +0 -0
  70. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/segments.py +0 -0
  71. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/traces/store.py +0 -0
  72. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/utils/__init__.py +0 -0
  73. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/utils/csv_io.py +0 -0
  74. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/utils/files.py +0 -0
  75. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/__init__.py +0 -0
  76. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/dataset/app.js +0 -0
  77. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/dataset/page.html +0 -0
  78. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/dataset/styles.css +0 -0
  79. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/dataset/vendor/echarts.min.js +0 -0
  80. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/failures/app.js +0 -0
  81. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/failures/page.html +0 -0
  82. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/failures/styles.css +0 -0
  83. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/trace/lanes.js +0 -0
  84. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/trace/vendor/markdown-it.LICENSE +0 -0
  85. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/assets/trace/vendor/markdown-it.min.js +0 -0
  86. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/dataset.py +0 -0
  87. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/failures.py +0 -0
  88. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/fields.py +0 -0
  89. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/history.py +0 -0
  90. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/render.py +0 -0
  91. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/timeline.py +0 -0
  92. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/visualize/trace.py +0 -0
  93. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/agent_tune_kit/workspace.py +0 -0
  94. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/assets/agent_tune_kit_evaluation.png +0 -0
  95. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/assets/agent_tune_kit_trace.png +0 -0
  96. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/assets/arch-en.png +0 -0
  97. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/assets/arch.png +0 -0
  98. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/assets/architecture.en.svg +0 -0
  99. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/assets/architecture.svg +0 -0
  100. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/ato-atk-project-boundary-recommendation-2026-11-05.md +0 -0
  101. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/cli-skill-modularization-plan.md +0 -0
  102. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/codex_agent_tuning_prd.md +0 -0
  103. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/execution-evidence-and-tracing-plan.md +0 -0
  104. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/ground-truth-and-grading-assessment-2026-10-25.md +0 -0
  105. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/local-context.md +0 -0
  106. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/shared-versioning-and-confirmation.md +0 -0
  107. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/skill-template-pack-usage.md +0 -0
  108. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/surginame-atk-capability-review-2026-10-27.md +0 -0
  109. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/trace-change-verification-readability-diagnosis.md +0 -0
  110. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/trace-swimlane-design.md +0 -0
  111. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/trace-tuning-validation.md +0 -0
  112. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/docs/truth-and-assessment.md +0 -0
  113. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/check-release.py +0 -0
  114. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/dev-refresh-install.sh +0 -0
  115. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/install_plugin.py +0 -0
  116. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/publish-pypi.sh +0 -0
  117. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/publish-release.py +0 -0
  118. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/release-version.py +0 -0
  119. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/scripts/release-version.sh +0 -0
  120. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-dataset/SKILL.md +0 -0
  121. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-dataset/scripts/update_dataset.py +0 -0
  122. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-diagnose/SKILL.md +0 -0
  123. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-eval/SKILL.md +0 -0
  124. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-init/SKILL.md +0 -0
  125. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-optimize/SKILL.md +0 -0
  126. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-report/SKILL.md +0 -0
  127. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-trace-capture/SKILL.md +0 -0
  128. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-visualize-dataset/SKILL.md +0 -0
  129. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-visualize-dataset/scripts/generate_dataset_browser.py +0 -0
  130. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-visualize-failures/SKILL.md +0 -0
  131. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/skills/atk-visualize-failures/scripts/generate_failure_browser.py +0 -0
  132. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/templates/.atk/runner/eval_runner.py.md +0 -0
  133. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/templates/.atk/runner/failure_rule.py.md +0 -0
  134. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/templates/.atk/runner/import_evidence.py.md +0 -0
  135. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/templates/.gitkeep +0 -0
  136. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/__init__.py +0 -0
  137. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/conftest.py +0 -0
  138. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/fixtures/legacy_eval_runner.py.txt +0 -0
  139. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/fixtures/managed_v1.py.md +0 -0
  140. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_assessment_contract.py +0 -0
  141. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_atk_init_contract.py +0 -0
  142. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_cli_workflow.py +0 -0
  143. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_dataset_updates.py +0 -0
  144. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_diagnosis.py +0 -0
  145. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_evidence_import.py +0 -0
  146. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_evidence_judgments.py +0 -0
  147. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_evidence_packaging.py +0 -0
  148. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_evidence_versions.py +0 -0
  149. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_execution_evidence.py +0 -0
  150. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_execution_outcomes.py +0 -0
  151. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_execution_tracing.py +0 -0
  152. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_generate_dataset_browser.py +0 -0
  153. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_generate_failure_browser.py +0 -0
  154. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_judge_calibration.py +0 -0
  155. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_local_context.py +0 -0
  156. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_plugin_cache.py +0 -0
  157. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_runner_output_columns.py +0 -0
  158. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_runner_template_row_logging.py +0 -0
  159. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_runtime_contracts.py +0 -0
  160. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_snapshot_reads.py +0 -0
  161. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_capture.py +0 -0
  162. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_categories.py +0 -0
  163. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_cli.py +0 -0
  164. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_langfuse.py +0 -0
  165. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_progress.py +0 -0
  166. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_query.py +0 -0
  167. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_records.py +0 -0
  168. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_segments.py +0 -0
  169. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_trace_store.py +0 -0
  170. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_truth_browser.py +0 -0
  171. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_truth_cli.py +0 -0
  172. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_truth_contract.py +0 -0
  173. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_truth_runner.py +0 -0
  174. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_v2_progress.py +0 -0
  175. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/test_visualize_trace.py +0 -0
  176. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/trace-event-wheel.cjs +0 -0
  177. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/trace-preview-scroll.cjs +0 -0
  178. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/trace-selection-reason.cjs +0 -0
  179. {agent_tune_kit-2.3.3 → agent_tune_kit-2.3.4}/tests/trace-stage-cards.cjs +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tune-kit",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
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.3
3
+ Version: 2.3.4
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
@@ -1,3 +1,3 @@
1
1
  """Agent Tune Kit package."""
2
2
 
3
- __version__ = "2.3.3"
3
+ __version__ = "2.3.4"
@@ -93,3 +93,12 @@ select:hover { border-color: #9baebb }
93
93
  .drawer.above-record { inset: 0 0 auto; border-radius: 0 0 12px 12px; border-top: 0; border-bottom: 1px solid var(--line) }
94
94
  .drawer-resizer { display: none }
95
95
  }
96
+
97
+ /* Project attribution belongs to issues; the menu is shared by both issue views. */
98
+ .project-choice { position: relative; margin-bottom: 7px; font-size: 13px }
99
+ .project-choice summary { height: 34px; padding: 6px 12px; border: 1px solid #cdd8e1; border-radius: 6px; background: white }
100
+ .project-choice summary:hover { border-color: #9baebb }
101
+ .project-menu { position: absolute; right: 0; z-index: 20; width: min(300px, 80vw); max-height: 320px; overflow: auto; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: white; box-shadow: 0 6px 20px #0002 }
102
+ .project-menu p { margin: 0 0 8px }
103
+ #project-filter label { display: flex; align-items: center; gap: 8px; padding: 6px 0 }
104
+ #project-filter input { width: 16px; height: 16px; min-width: 16px; padding: 0 }
@@ -29,10 +29,25 @@
29
29
  return roots.some(root => { let current = root.id; const seen = new Set(); while (current && !seen.has(current)) { if (current === id) return true; seen.add(current); current = data.projects?.[current]?.parent_id; } return false; });
30
30
  });
31
31
  const projectText = item => (item.root_projects || []).map(p => p.name + (p.pending ? "(待确认)" : "")).join("、") || "待归因";
32
- for (const p of Object.values(data.projects || {})) { const option = el("option", p.abbreviation || p.name); option.value = p.id; $("project-filter").append(option); }
33
- for (const option of $("project-filter").options) option.selected = projectSelection.has(option.value);
34
- const projectOverview = el("p", `根因项目:${[...new Set(data.issues.map(projectText))].join("、") || "待归因"}`);
35
- $("project-filter").parentElement.append(projectOverview);
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(); };
46
+ $("project-choice").onkeydown = event => { if (event.key === "Escape") { $("project-choice").open = false; $("project-summary").focus(); } };
47
+ document.addEventListener("click", event => {
48
+ if (!event.target.closest("#project-choice")) $("project-choice").open = false;
49
+ });
50
+
36
51
  const filters = ["trace-filter", "type-filter", "trace-search", "record-mode"];
37
52
  const list = $("trace-list"), scroller = $("trace-scroll");
38
53
  const scope = () => $("trace-filter").value === "" ? null : Number($("trace-filter").value);
@@ -291,7 +306,7 @@
291
306
  if (snapshot?.project_links) {
292
307
  const entries = new Map((snapshot.project_registry?.projects || []).map(p => [p.id, p]));
293
308
  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" ? "" : "(待确认)"); });
294
- body.prepend(el("p", `诊断时的根因项目:${names.join("、") || "待归因"}`));
309
+ body.prepend(el("p", `诊断时的关联项目:${names.join("、") || "待归因"}`));
295
310
  }
296
311
  const name = diagnosis.reference.path.split("/").pop();
297
312
  details.append(el("summary", `展开原诊断 · ${name}`), body);
@@ -391,7 +406,7 @@
391
406
  $("drawer-content").append(tabs);
392
407
  if (stage === "issue") {
393
408
  const description = section("问题描述");
394
- description.append(el("p", `根因项目:${projectText(item)}`), el("p", issue.description || issue.title));
409
+ description.append(el("p", `关联项目:${projectText(item)}`), el("p", issue.description || issue.title));
395
410
  $("drawer-content").append(description);
396
411
  if (typeof issue.selection_reason === "string" && issue.selection_reason.trim()) {
397
412
  const reason = section("保留理由");
@@ -1013,9 +1028,9 @@
1013
1028
  const head = el("div", undefined, "issue-card-head");
1014
1029
  head.append(el("span", issue.id, "issue-id-text"), el("span", undefined, `progress-dot ${progress.klass}`));
1015
1030
  const body = el("div", undefined, "issue-card-body");
1016
- const projects = el("p", `根因项目:${projectText(item)}`);
1031
+ const projects = el("p", `关联项目:${projectText(item)}`);
1017
1032
  if (data.dashboard.available) for (const project of item.root_projects || []) {
1018
- const link = el("a", project.name); link.href = dashboardHash(`view=session&project=${encodeURIComponent(project.id)}`); projects.append(link);
1033
+ const link = el("a", project.name); link.href = dashboardHash(`view=category&project=${encodeURIComponent(project.id)}`); projects.append(link);
1019
1034
  }
1020
1035
  body.append(projects);
1021
1036
  body.append(el("h3", issue.title || "问题"), el("div", progress.label, "issue-progress"));
@@ -1107,7 +1122,7 @@
1107
1122
  $("capture-filter").closest("label").hidden = Boolean(currentCapture?.current);
1108
1123
  options($("capture-filter"), data.captures.map(c => c.id));
1109
1124
  if (currentCapture) $("capture-filter").value = currentCapture.id;
1110
- $("project-filter").onchange = () => { projectSelection.clear(); for (const option of $("project-filter").selectedOptions) projectSelection.add(option.value); 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(); };
1125
+ $("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(); };
1111
1126
  $("capture-filter").onchange = () => { investigation = null; scopeBefore = null; railFilter = null; chooseCapture(); renderIssueRail(); renderTraces(); };
1112
1127
  $("trace-filter").onchange = () => focusTrace(scope());
1113
1128
  for (const id of ["trace-search","type-filter","record-mode"]) $(id).addEventListener(id === "trace-search" ? "input" : "change", () => renderTraces());
@@ -21,7 +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
- <label>根因项目(含子项目)<select id="project-filter" multiple aria-label="根因项目"><option value="~unattributed">待归因</option></select></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
25
  <label><span class="sr-only">状态</span><select id="issue-status"></select></label>
26
26
  </div>
27
27
  <div id="issue-rail-list" class="rail-list"></div>
@@ -30,6 +30,9 @@ body.rail-collapsed #rail-toggle { transform: rotate(180deg) }
30
30
  .rail-filters .search-label { flex: 1; min-width: 0 }
31
31
  .rail-filters input, .rail-filters select { width: 100%; height: 32px; font-size: 12px; padding: 0 8px }
32
32
  .rail-filters select { width: auto; min-width: 88px }
33
+ .rail-filters .project-choice { margin-bottom: 0; font-size: 12px }
34
+ .rail-filters .project-choice summary { height: 32px; padding: 6px 8px; white-space: nowrap }
35
+ .rail-filters .project-menu { left: 0; right: auto }
33
36
  .rail-list { flex: 1; min-height: 0; overflow: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px }
34
37
  .issue-card { display: flex; flex-direction: column; gap: 6px; width: 100%; text-align: left; padding: 10px }
35
38
  .issue-card-head { justify-content: space-between; gap: 6px; width: 100% }
@@ -11,14 +11,28 @@
11
11
  const button = (text, action, className) => { const node = el("button", text, className); node.type = "button"; node.onclick = action; return node; };
12
12
 
13
13
  const projectSelection = new Set(new URLSearchParams(location.hash.slice(1)).getAll("project"));
14
- const projectMatches = item => !projectSelection.size || [...projectSelection].some(id => {
14
+ const projectMatches = item => state.view === "session" || !projectSelection.size || [...projectSelection].some(id => {
15
15
  const roots = item.root_projects || [];
16
16
  if (id === "~unattributed") return !roots.length;
17
17
  return roots.some(root => { let current = root.id; const seen = new Set(); while (current && !seen.has(current)) { if (current === id) return true; seen.add(current); current = data.projects?.[current]?.parent_id; } return false; });
18
18
  });
19
19
  const projectText = item => (item.root_projects || []).map(p => p.name + (p.pending ? "(待确认)" : "")).join("、") || "待归因";
20
- for (const p of Object.values(data.projects || {})) { const option = el("option", p.abbreviation || p.name); option.value = p.id; $("project-filter").append(option); }
21
- for (const option of $("project-filter").options) option.selected = projectSelection.has(option.value);
20
+ const projectInputs = [];
21
+ for (const p of [{id: "~unattributed", name: "待归因"}, ...Object.values(data.projects || {})]) {
22
+ const label = el("label"), input = el("input");
23
+ input.type = "checkbox"; input.value = p.id;
24
+ input.checked = projectSelection.has(p.id);
25
+ label.append(input, document.createTextNode(p.abbreviation || p.name));
26
+ $("project-filter").append(label); projectInputs.push(input);
27
+ input.onchange = () => $("project-filter").onchange();
28
+ }
29
+ const updateProjectSummary = () => {
30
+ $("project-summary").textContent = projectSelection.size ? `关联项目:已选 ${projectSelection.size} 项` : "关联项目:全部";
31
+ };
32
+ updateProjectSummary();
33
+ $("project-clear").onclick = () => { projectInputs.forEach(input => { input.checked = false; }); $("project-filter").onchange(); };
34
+ $("project-choice").onkeydown = event => { if (event.key === "Escape") { $("project-choice").open = false; $("project-summary").focus(); } };
35
+
22
36
 
23
37
  const NONE = "~uncategorized"; // cannot collide with a kebab-case category ID
24
38
  const ORDER = new Map(data.progress_legend.map((entry, index) => [entry.key, index]));
@@ -37,7 +51,6 @@
37
51
  return `${part.month}-${part.day} ${part.hour === "24" ? "00" : part.hour}:${part.minute}`;
38
52
  };
39
53
  const sourceText = source => [source.type, source.environment].filter(Boolean).join(" · ") || "来源未记录";
40
- const scaleText = s => s.traces != null && s.observations != null ? `${s.traces} Trace · ${s.observations} 观测` : `${s.files} 个归档文件`;
41
54
  const statusText = value => STATUS[value] || value || "未记录";
42
55
 
43
56
  // --- derived search text and source scope ---------------------------------------------------------------------
@@ -53,7 +66,7 @@
53
66
  const matchIssue = (s, issue) =>
54
67
  projectMatches(issue) && (!state.progress || issue.progress === state.progress) && (!state.q || s._text.includes(state.q) || issue._text.includes(state.q));
55
68
  const matchSession = s => s.issues.some(issue => matchIssue(s, issue)) ||
56
- (!s.issues.length && !projectSelection.size && !state.progress && (!state.q || s._text.includes(state.q)));
69
+ (!s.issues.length && (state.view === "session" || !projectSelection.size) && !state.progress && (!state.q || s._text.includes(state.q)));
57
70
  const byProgress = (a, b) => ORDER.get(a.progress) - ORDER.get(b.progress) || a.id.localeCompare(b.id, undefined, {numeric: true});
58
71
 
59
72
  // --- small building blocks ------------------------------------------------------------------------------------
@@ -181,7 +194,6 @@
181
194
  }
182
195
  card.append(el("div", `${sourceText(s.source)} · 更新 ${when(s.updated_ms)}`, "card-meta"));
183
196
  const matched = s.issues.filter(issue => matchIssue(s, issue));
184
- card.append(el("p", `根因项目:${[...new Set(matched.map(projectText))].join("、") || "待归因"}`));
185
197
  if (matched.length) {
186
198
  const counts = countBy(matched);
187
199
  const row = el("div", undefined, "card-count");
@@ -233,7 +245,7 @@
233
245
  const counts = countBy(items.map(item => item.issue));
234
246
  const row = el("div", undefined, "card-count");
235
247
  row.append(el("strong", `${new Set(items.map(item => item.s.dir)).size} 个会话 · ${items.length} 个问题`), el("span", `最近 ${when(latestOf(items) < 0 ? null : latestOf(items))}`, "muted"));
236
- card.append(el("p", `根因项目:${[...new Set(items.map(({issue}) => projectText(issue)))].join("、")}`));
248
+ card.append(el("p", `关联项目:${[...new Set(items.map(({issue}) => projectText(issue)))].join("、")}`));
237
249
  card.append(row, miniBar(counts), el("div", topProgress(counts), "card-meta"));
238
250
  return card;
239
251
  }
@@ -261,12 +273,12 @@
261
273
  function issueRow(s, issue, withCategory) {
262
274
  const row = el("div", undefined, "issue-row");
263
275
  const dot = el("span", undefined, `progress-dot ${issue.progress}`); dot.title = LABEL.get(issue.progress);
264
- const href = `${s.href}#issue=${encodeURIComponent(issue.id)}${[...projectSelection].map(id => `&project=${encodeURIComponent(id)}`).join("")}`;
276
+ const href = `${s.href}#issue=${encodeURIComponent(issue.id)}${(state.view === "category" ? [...projectSelection] : []).map(id => `&project=${encodeURIComponent(id)}`).join("")}`;
265
277
  const title = el("a", issue.title || issue.id, "row-title"); title.href = href; title.title = `${issue.id} ${issue.title}`;
266
278
  const jump = el("a", "↗", "row-jump"); jump.href = href; jump.setAttribute("aria-label", `在会话页打开 ${issue.id}`);
267
279
  // Title gets the full first line and may wrap; category and progress sit on their own line beneath it.
268
280
  const meta = el("div", undefined, "row-meta");
269
- meta.append(el("span", `根因项目:${projectText(issue)}`, "project-tag"));
281
+ meta.append(el("span", `关联项目:${projectText(issue)}`, "project-tag"));
270
282
  if (withCategory) meta.append(categoryChip(issue.category));
271
283
  meta.append(el("span", LABEL.get(issue.progress), "row-progress"));
272
284
  row.append(dot, el("span", shortIssue(issue.id), "issue-id-text"), title, jump, meta);
@@ -275,7 +287,7 @@
275
287
  function issueList(box, s, issues, withCategory) {
276
288
  const rows = issues.filter(issue => matchIssue(s, issue)).sort(byProgress);
277
289
  const list = el("div", undefined, "issue-list");
278
- if (!rows.length) list.append(el("div", issues.length ? "没有符合筛选条件的问题。" : "无保留问题。", "empty"));
290
+ if (!rows.length) list.append(el("div", issues.length ? "没有符合筛选条件的问题。" : "无保留问题(不代表会话正确)。", "empty"));
279
291
  for (const issue of rows) list.append(issueRow(s, issue, withCategory));
280
292
  box.append(list);
281
293
  return {shown: rows.length, total: issues.length};
@@ -283,40 +295,35 @@
283
295
  const textDetails = (title, build) => { const node = el("details", undefined, "detail-section"); node.append(el("summary", title)); build(node); return node; };
284
296
  const bullets = (node, items) => { const list = el("ul"); for (const item of items) list.append(el("li", item)); node.append(list); };
285
297
 
286
- function sessionDrawer(s, box) {
287
- $("drawer-title").textContent = s.session_id;
288
- const actions = el("div", undefined, "drawer-actions");
289
- actions.append(copyButton(s.session_id, "复制 Session ID", "复制 Session ID"));
290
- if (!s.error) { const open = el("a", "打开会话页 ↗", "link-button"); open.href = s.href; actions.append(open); }
291
- box.append(actions);
292
- if (s.error) { box.append(el("p", `会话记录不可读取:${s.error}`, "warning")); return; }
293
- const meta = el("div", undefined, "drawer-meta");
294
- for (const text of [sourceText(s.source), scaleText(s), `归档 ${when(s.archived_ms)}`, `更新 ${when(s.updated_ms)}`]) meta.append(el("span", text));
295
- box.append(meta);
296
- const origin = [s.source.instance, s.source.project].filter(Boolean).join(" / ");
297
- if (origin) box.append(el("p", origin, "mono muted"));
298
- if (!s.preview.exists || s.preview.stale) {
299
- const note = el("p", s.preview.exists ? "会话页早于最近一次记录写入。刷新命令:" : "会话页尚未生成。生成命令:", "warning");
300
- note.append(el("code", `atk visualize trace --session-id ${s.session_id} --overwrite --no-open`));
301
- box.append(note);
298
+ // The session drawer is a preview: the header carries identity and the way in; below it at most a few one-line
299
+ // notes appear, and only when something makes the issue list untrustworthy or needs an action. Source, scale and
300
+ // full detection coverage live on the session page (「会话信息」).
301
+ function sessionNotes(s, box) {
302
+ if (!s.detection) box.append(el("p", "尚未检测,问题列表为空不代表会话没有问题。", "warning"));
303
+ else if (s.detection.status !== "completed") {
304
+ box.append(el("p", `最近一次检测${statusText(s.detection.status)},问题可能不全;检测缺口见会话页「会话信息」。`, "warning"));
302
305
  }
303
306
  if (s.problem_count) {
304
- box.append(textDetails(`记录读取失败 ${s.problem_count}`, node => {
305
- bullets(node, s.problems);
306
- if (s.problem_count > s.problems.length) node.append(el("p", `另有 ${s.problem_count - s.problems.length} 条未显示。`, "muted"));
307
- }));
307
+ const node = textDetails(`${s.problem_count} 条记录读取失败`, inner => {
308
+ bullets(inner, s.problems);
309
+ if (s.problem_count > s.problems.length) inner.append(el("p", `另有 ${s.problem_count - s.problems.length} 条未显示。`, "muted"));
310
+ });
311
+ node.classList.add("warning"); box.append(node);
312
+ }
313
+ if (!s.preview.exists || s.preview.stale) {
314
+ const command = `atk visualize trace --session-id ${s.session_id} --overwrite --no-open`;
315
+ const note = el("p", s.preview.exists ? "会话页已过期,不含最近的记录。" : "会话页尚未生成。", "warning note-row");
316
+ const copy = copyButton(command, s.preview.exists ? "复制刷新命令" : "复制生成命令", s.preview.exists ? "复制刷新命令" : "复制生成命令");
317
+ copy.title = command; note.append(copy); box.append(note);
308
318
  }
309
- box.append(textDetails("检测范围与缺口", node => {
310
- if (s.detection) {
311
- node.append(el("p", `检测状态:${statusText(s.detection.status)}`));
312
- if (s.detection.focus) node.append(el("p", `关注点:${s.detection.focus}`));
313
- if (s.detection.gaps.length) { node.append(el("p", "检测缺口:")); bullets(node, s.detection.gaps); }
314
- } else node.append(el("p", "尚未记录检测。", "muted"));
315
- const coverage = {complete: "完整", partial: "部分", unknown: "未确定"}[s.capture.completeness] || s.capture.completeness || "未记录";
316
- node.append(el("p", `采集状态:${statusText(s.capture.status)} · 完整性:${coverage}`));
317
- if (s.capture.gaps.length) { node.append(el("p", "采集缺口:")); bullets(node, s.capture.gaps); }
318
- if (!s.issues.length) node.append(el("p", "没有保留问题不代表会话正确,请结合检测范围与缺口判断。", "muted"));
319
- }));
319
+ }
320
+ function sessionDrawer(s, box) {
321
+ $("drawer-title").textContent = s.session_id;
322
+ $("drawer-title-tools").append(copyButton(s.session_id, "复制 Session ID"));
323
+ if (s.error) { box.append(el("p", `会话记录不可读取:${s.error}`, "warning")); return; }
324
+ const open = el("a", "打开会话页 ↗", "link-button primary"); open.href = s.href;
325
+ $("drawer-tools").append(open);
326
+ sessionNotes(s, box);
320
327
  const heading = el("div", undefined, "list-heading");
321
328
  const label = el("span", "问题"); heading.append(label); box.append(heading);
322
329
  const counts = issueList(box, s, s.issues, true);
@@ -356,7 +363,7 @@
356
363
  if (state.view === "session" && !session) { state.drawer = null; drawer.hidden = true; shown = null; return; }
357
364
  const id = `${state.view}:${state.drawer}`, fresh = shown !== id;
358
365
  const box = $("drawer-content"), keep = fresh ? 0 : box.scrollTop;
359
- box.replaceChildren();
366
+ box.replaceChildren(); $("drawer-title-tools").replaceChildren(); $("drawer-tools").replaceChildren();
360
367
  if (session) sessionDrawer(session, box); else categoryDrawer(state.drawer, box);
361
368
  drawer.hidden = false; box.scrollTop = keep; shown = id;
362
369
  if (fresh) $("drawer-close").focus({preventScroll: true});
@@ -371,7 +378,7 @@
371
378
 
372
379
  // --- navigation -----------------------------------------------------------------------------------------------
373
380
  function syncHash() {
374
- const parts = [`view=${state.view}`, ...[...projectSelection].map(id => `project=${encodeURIComponent(id)}`)];
381
+ const parts = [`view=${state.view}`, ...(state.view === "category" ? [...projectSelection].map(id => `project=${encodeURIComponent(id)}`) : [])];
375
382
  if (state.drawer !== null) parts.push(`${state.view}=${encodeURIComponent(state.drawer)}`);
376
383
  const next = `#${parts.join("&")}`;
377
384
  if (location.hash !== next) history.replaceState(null, "", next);
@@ -383,8 +390,8 @@
383
390
  function applyHash() {
384
391
  const params = new URLSearchParams(location.hash.slice(1));
385
392
  projectSelection.clear(); params.getAll("project").forEach(id => projectSelection.add(id));
386
- for (const option of $("project-filter").options) option.selected = projectSelection.has(option.value);
387
- const view = params.get("view") === "category" ? "category" : "session";
393
+ for (const input of projectInputs) input.checked = projectSelection.has(input.value); updateProjectSummary();
394
+ const view = params.get("view") === "category" || (projectSelection.size && !params.get("session")) ? "category" : "session";
388
395
  let drawer = null;
389
396
  if (view === "session") {
390
397
  const key = params.get("session");
@@ -398,6 +405,7 @@
398
405
  }
399
406
 
400
407
  function render() {
408
+ $("project-choice").hidden = state.view === "session";
401
409
  renderTabs(); renderProgress(); renderNote();
402
410
  const grid = $("dash-grid"), strip = $("dash-strip");
403
411
  grid.replaceChildren(); strip.replaceChildren();
@@ -430,7 +438,7 @@
430
438
  $(id).onclick = () => setView(view);
431
439
  $(id).onkeydown = onTabKey;
432
440
  }
433
- $("project-filter").onchange = () => { projectSelection.clear(); for (const option of $("project-filter").selectedOptions) projectSelection.add(option.value); syncHash(); render(); };
441
+ $("project-filter").onchange = () => { projectSelection.clear(); for (const input of projectInputs) if (input.checked) projectSelection.add(input.value); updateProjectSummary(); syncHash(); render(); };
434
442
  $("dash-search").oninput = event => { state.q = event.target.value.trim().toLowerCase(); render(); };
435
443
  $("drawer-close").onclick = closeDrawer;
436
444
  document.addEventListener("keydown", event => {
@@ -445,6 +453,7 @@
445
453
  let pressedOutside = false;
446
454
  document.addEventListener("pointerdown", event => { pressedOutside = state.drawer !== null && dismissable(event.target); });
447
455
  document.addEventListener("click", event => {
456
+ if (!event.target.closest("#project-choice")) $("project-choice").open = false;
448
457
  const close = pressedOutside && state.drawer !== null && dismissable(event.target);
449
458
  pressedOutside = false;
450
459
  if (close) closeDrawer();
@@ -20,7 +20,7 @@
20
20
  <button id="tab-category" type="button" role="tab" aria-controls="dash-panel">异常</button>
21
21
  </div>
22
22
  <label class="search-label"><span class="sr-only">搜索</span><input id="dash-search" type="search" placeholder="搜索 Session ID、问题编号、标题、分类"></label>
23
- <label>根因项目(含子项目)<select id="project-filter" multiple aria-label="根因项目"><option value="~unattributed">待归因</option></select></label>
23
+ <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>
24
24
  <label id="source-choice" hidden><span class="sr-only">来源</span><select id="source-filter"></select></label>
25
25
  </div>
26
26
  <div id="dash-panel" class="dash-panel" role="tabpanel" aria-labelledby="tab-session">
@@ -35,7 +35,10 @@
35
35
  </main>
36
36
  <aside id="drawer" class="drawer" role="dialog" aria-modal="false" aria-labelledby="drawer-title" hidden tabindex="-1">
37
37
  <div class="drawer-resizer" id="drawer-resizer" aria-hidden="true"></div>
38
- <div class="drawer-header"><h2 id="drawer-title"></h2><button id="drawer-close" type="button" aria-label="关闭详情">关闭 ×</button></div>
38
+ <div class="drawer-header">
39
+ <div class="drawer-heading"><h2 id="drawer-title"></h2><span id="drawer-title-tools"></span></div>
40
+ <div class="drawer-tools"><span id="drawer-tools"></span><button id="drawer-close" type="button" aria-label="关闭详情">关闭 ×</button></div>
41
+ </div>
39
42
  <div id="drawer-content" class="drawer-content"></div>
40
43
  </aside>
41
44
  <script id="dashboard-data" type="application/json">__DATA__</script>
@@ -12,6 +12,8 @@
12
12
  .dash-controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; border-bottom: 1px solid var(--line) }
13
13
  .dash-controls .search-label { flex: 1 1 260px; max-width: 440px; min-width: 0; margin-left: auto }
14
14
  .dash-controls .search-label, #source-choice { margin-bottom: 7px }
15
+ /* Keep the toolbar geometry when the issue-only project filter is hidden. */
16
+ .dash-controls #project-choice[hidden] { display: block !important; visibility: hidden }
15
17
  .view-tabs { display: flex; gap: 4px }
16
18
  .view-tabs [role=tab] { height: 40px; margin-bottom: -1px; padding: 0 16px; border: 0; border-bottom: 2px solid transparent; border-radius: 6px 6px 0 0; background: transparent; color: var(--muted); font-weight: 600 }
17
19
  .view-tabs [role=tab]:hover { color: var(--ink); background: #edf1f4 }
@@ -40,7 +42,7 @@
40
42
  .filter-note { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted) }
41
43
  .filter-note button { font-size: 12px; padding: 2px 8px }
42
44
 
43
- .dash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; align-items: start }
45
+ .dash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; align-items: stretch }
44
46
  .dash-card { position: relative; display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 12px 14px }
45
47
  .card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0 }
46
48
  .card-head .card-open { flex: 1 1 0 }
@@ -70,7 +72,19 @@ button.category-tag:hover { background: #ffe8d2 }
70
72
  .empty-dash { padding: 28px; text-align: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 8px; background: white }
71
73
 
72
74
  .drawer-meta { font-size: 12px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 2px 12px; margin-bottom: 10px }
73
- .drawer-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px }
75
+ /* Header: Session ID with its copy icon on the left; the way into the session page and close on the right. */
76
+ .drawer-heading { display: flex; align-items: center; gap: 4px; min-width: 0 }
77
+ .drawer-heading h2 { min-width: 0 }
78
+ .drawer-tools { display: flex; align-items: center; gap: 8px; flex-shrink: 0 }
79
+ .drawer-tools:has(> span:empty) { gap: 0 }
80
+ .link-button.primary { font-weight: 600; background: var(--tint); border-color: #eabda0; white-space: nowrap }
81
+ .link-button.primary:hover { background: #ffe9d6 }
82
+ /* Notes only appear when something is off; each stays one compact line. */
83
+ .drawer-content > .warning { font-size: 12px; margin: 0 0 8px; padding: 6px 10px }
84
+ .drawer-content > details.warning { background: #fff9e8; border-color: #eed79d }
85
+ .drawer-content > details.warning summary { font-size: 12px; color: #804810 }
86
+ .note-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px }
87
+ .note-row button { font-size: 12px; padding: 1px 8px }
74
88
  .link-button { display: inline-block; border: 1px solid var(--line); border-radius: 6px; background: white; padding: 4px 10px; font-size: 12px; color: var(--accent); text-decoration: none }
75
89
  .link-button:hover { background: var(--tint); border-color: #eabda0 }
76
90
  .drawer-content details.detail-section { padding: 8px 12px }
@@ -100,10 +114,15 @@ button.category-tag:hover { background: #ffe8d2 }
100
114
  @media (max-width: 800px) {
101
115
  /* The drawer is a bottom sheet here, so it rises instead (reduced motion still zeroes the duration). */
102
116
  .drawer:not([hidden]) { animation-name: sheet-in }
117
+ /* A full Session ID needs the whole sheet width: the buttons drop onto their own row instead of squeezing it. */
118
+ .drawer-header { flex-wrap: wrap; row-gap: 8px }
119
+ .drawer-heading { flex: 1 1 100% }
120
+ .drawer-heading h2 { font-size: 14px }
121
+ .drawer-tools { margin-left: auto }
103
122
  .dash-grid { grid-template-columns: 1fr }
104
123
  /* Search wraps under the tabs here, so the tab line moves onto the tab row itself. */
105
124
  .dash-controls { border-bottom: 0 }
106
125
  .view-tabs { flex-basis: 100%; border-bottom: 1px solid var(--line) }
107
126
  .dash-controls .search-label { max-width: none; margin-left: 0 }
108
- .dash-controls .search-label, #source-choice { margin-bottom: 0 }
127
+ .dash-controls .search-label, .dash-controls .project-choice, #source-choice { margin-bottom: 0 }
109
128
  }
@@ -1,6 +1,6 @@
1
1
  """Project-wide dashboard over every archived session, written to ``.atk/traces/index.html``.
2
2
 
3
- The page only summarizes: identity, scale, actual progress and category per issue, plus a link into each session's own
3
+ The page only summarizes: identity, actual progress and category per issue, whether the latest detection finished, plus a link into each session's own
4
4
  preview. It reads saved records through ``records.status`` (which already verifies evidence digests) and never opens
5
5
  raw trace bodies, so it stays small and fast. A damaged session or issue is reported on its own entry and cannot hide
6
6
  the healthy ones.
@@ -45,12 +45,6 @@ def _text(value, limit=TEXT_LIMIT):
45
45
  return value if len(value) <= limit else value[: limit - 1] + "…"
46
46
 
47
47
 
48
- def _texts(value):
49
- if not isinstance(value, list):
50
- return []
51
- return [text for text in (_text(item, ITEM_LIMIT) for item in value[:LIST_LIMIT]) if text]
52
-
53
-
54
48
  def _ms(seconds):
55
49
  return None if seconds is None else int(seconds * 1000)
56
50
 
@@ -143,8 +137,6 @@ def _session_entry(session_dir):
143
137
  )
144
138
  return entry
145
139
  session = status["session"]
146
- archive = session.get("archive") if isinstance(session.get("archive"), dict) else {}
147
- completeness = archive.get("completeness") if isinstance(archive.get("completeness"), dict) else {}
148
140
  detections = [item for item in session.get("detections", []) if isinstance(item, dict)]
149
141
  issues, issue_stamps = _issue_entries(session_dir, status)
150
142
  archived = parse_instant(session.get("created_at"))
@@ -153,31 +145,14 @@ def _session_entry(session_dir):
153
145
  registry = session_dir.parent / "projects.json"
154
146
  if registry.is_file():
155
147
  updated = max(updated or 0, registry.stat().st_mtime)
148
+ # The drawer is a preview: it only needs to say whether the issue list can be trusted. Scale, capture coverage,
149
+ # focus and gap details stay on the session page, which shows every detection in full.
156
150
  last = detections[-1] if detections else None
157
- scope = last.get("scope") if last and isinstance(last.get("scope"), dict) else {}
158
- traces, observations = completeness.get("trace_count"), completeness.get("observation_count")
159
151
  entry.update(
160
152
  session_id=session["session_id"],
161
153
  source={key: _text(session.get("source", {}).get(key), ITEM_LIMIT) for key in SAFE_SOURCE_FIELDS},
162
- archived_ms=_ms(archived),
163
154
  updated_ms=_ms(updated),
164
- traces=traces if type(traces) is int else None,
165
- observations=observations if type(observations) is int else None,
166
- files=len(archive["files"]) if isinstance(archive.get("files"), list) else len(session.get("captures", [])),
167
- capture={
168
- "status": _text(archive.get("status"), 40),
169
- "completeness": _text(completeness.get("status"), 40),
170
- "gaps": _texts(completeness.get("gaps")),
171
- },
172
- detection=(
173
- {
174
- "status": _text(last.get("status"), 40),
175
- "focus": _text(scope.get("focus")),
176
- "gaps": _texts(last.get("gaps")),
177
- }
178
- if last
179
- else None
180
- ),
155
+ detection={"status": _text(last.get("status"), 40)} if last else None,
181
156
  issues=issues,
182
157
  problems=[_problem(item) for item in status["warnings"][:LIST_LIMIT]],
183
158
  problem_count=len(status["warnings"]),
@@ -51,7 +51,7 @@
51
51
  | `3a735b2`:`records.py` 新增 `change_summary`、`verification_summary` 校验 | 大盘只在 `prepare_issue()` 增加 `category` 校验,不在同一函数 | 无冲突 |
52
52
  | `3a735b2`:`styles.css` 新增 `.stage-record`、`.change-item`、`.source-file` | 属于单会话页专用样式 | 抽取共享样式时留在 `trace/styles.css`,不进入 `base.css` |
53
53
  | `records.status()` 读取全部 Issue、改动和验证,并校验引用哈希;在 Magic 6 个会话上逐个调用,合计约 0.3 秒 | 大盘可以直接复用,不必另写读取逻辑 | 大盘数据构建基于 `status()` |
54
- | Magic 本地导入的 `archive.completeness` 有 `trace_count`、`observation_count`;其他来源不一定有 | 抽屉中的 Trace/观测数可能缺失 | 有就显示,没有时只显示归档文件数 |
54
+ | Magic 本地导入的 `archive.completeness` 有 `trace_count`、`observation_count`;其他来源不一定有。本地导入的完整性固定为“部分” | 规模和完整性在会话之间没有区分度,对预览没有帮助 | 大盘不展示,也不输出这些字段(2026-11-10 起);完整内容在会话页「会话信息」中 |
55
55
  | 全量测试 `uv run --frozen pytest`:407 通过。不加 `--frozen` 时,本机 uv 会把 `uv.lock` 的 registry 改写为带末尾斜杠,导致 `test_release_scripts.py` 的地址比对失败 | 开发中跑测试可能顺带改动 `uv.lock` | 开发与验收一律使用 `uv run --frozen`,不提交 `uv.lock` |
56
56
 
57
57
  ## 4. 数据契约
@@ -201,16 +201,26 @@
201
201
  点击卡片打开**会话抽屉**:
202
202
 
203
203
  ```text
204
- ┃ SES_2118585447553830912 ⧉ [打开会话页 ↗] ┃
205
- ┃ local · onl · 3 Trace · 1513 观测 · 归档 11-09 17:26 ┃
206
- ┃ ▸ 检测范围与缺口(折叠:focus、gaps、采集完整性) ┃
207
- ┃ ─ 问题 11 ──────────────────────────────────────────────────── ┃
204
+ ┃ SES_2118585447553830912 ⧉ [打开会话页 ↗] [关闭 ×] ┃
205
+ ┃ ⚠ 最近一次检测部分完成,问题可能不全;检测缺口见会话页「会话信息」 ┃
206
+ ┃ ─ 问题 9 ───────────────────────────────────────────────────── ┃
208
207
  ┃ ● 002 交付回复把列表项内边距误报为统一 16 未分类 待诊断 ↗ ┃
209
208
  ┃ ● 001 订单详情空态提示点击选单,但页面没有… 未分类 已诊断 ↗ ┃
210
209
  ```
211
210
 
212
- - 头部:完整 Session ID、来源、Trace/观测数(缺失时显示归档文件数)、归档时间,以及“打开会话页”。
213
- - 检测范围与缺口默认折叠。
211
+ 抽屉是进入会话页之前的预览,只回答两件事:这份问题列表能不能信,以及下一步要做什么(2026-11-10 起)。
212
+
213
+ - 头部只有完整 Session ID、复制图标、主按钮“打开会话页”和关闭。会话记录不可读取时没有“打开会话页”。
214
+ - 标题下方的提醒只在需要时出现,每条一行;全部正常时不显示,标题下方直接是问题列表:
215
+
216
+ | 条件 | 提醒 |
217
+ |---|---|
218
+ | 最近一次检测不是“已完成” | 最近一次检测{状态},问题可能不全;检测缺口见会话页「会话信息」 |
219
+ | 没有检测记录 | 尚未检测,问题列表为空不代表会话没有问题 |
220
+ | 有记录读取失败 | N 条记录读取失败(可展开明细) |
221
+ | 会话页缺失或过期 | 会话页尚未生成或已过期,附“复制生成命令 / 复制刷新命令”按钮 |
222
+
223
+ - 不展示来源、实例和项目 ID、Trace/观测数、归档和更新时间、关注点、检测与采集缺口:卡片上已有来源和更新时间,其余内容在会话页「会话信息」中完整展示。数据层只输出最近一次检测的 `status`。
214
224
  - 问题列表每行:进度点、编号、单行标题(悬停显示全文)、分类、进度、↗ 跳转。按进度排序,待处理的在前,同一进度内按编号。
215
225
 
216
226
  ### 7.3 异常维度
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## 1. 结论与已确认决策
6
6
 
7
+ 会话包含异常,具体异常关联项目。会话和异常分类均不保存独立项目归属,也不保存异常项目的并集。`source.project` 是轨迹来源命名空间,保持原义。项目多选使用可展开的复选菜单,提供清空、已选数量与键盘操作。
8
+
7
9
  在现有 Trace 流程内增加工程目录、Issue 的多工程归因,以及大盘和会话页的根因项目展示与筛选。初始化建立可复用的工程边界,诊断按证据决定归因;项目发现本身不产生归因。页面首先回答“问题出在哪个项目”,修复详情再说明“在哪里修改、为什么这样改”。
8
10
 
9
11
  | 决策 | 约定 | 来源 |
@@ -85,7 +87,7 @@ atk trace project --data <entry.json> --project-root <workspace> --json
85
87
 
86
88
  | 信息 | 表达的事实 | 呈现位置 |
87
89
  | --- | --- | --- |
88
- | 根因项目 | 根因实现/规则属于哪个工程;未证实的判断标为待确认 | 大盘、问题列表、会话概览及诊断详情 |
90
+ | 根因项目 | 根因实现/规则属于哪个工程;未证实的判断标为待确认 | 异常视图、问题列表及诊断详情 |
89
91
  | 修改位置与范围 | 计划或实际在哪个工程、哪些文件/行为上修复;与根因项目不同时说明原因 | 修复方案、实际改动与验证详情 |
90
92
 
91
93
  一条异常可以有多个根因项目,不强制选一个“主责任项目”。通常在根因项目内修改,不需要额外展示一组相同的“改进项目”标签。若权衡后选择其他项目修改,修复详情用一句话说明目标和理由;不能因为修复落在其他项目就改变根因归属。根因未知但已有可行防护方案时,可以记录该方案,根因仍保持未确定。
@@ -150,7 +152,7 @@ $atk-trace-diagnose 调查 <session-id>,优先检查 APE 和 OT,
150
152
 
151
153
  ### 大盘
152
154
 
153
- 继续提供“会话/异常分类”两个视图,只增加“根因项目”多选。卡片和问题行的项目标签都表示根因归属;不出现“角色”“改进项目”控件,不新增按修改项目筛选或统计的入口。新增的工程名称/别名搜索索引也只取根因关联,通用标题等文本搜索沿用现有行为。沿用现有组件、样式与离线页面,不新增 UI 依赖。
155
+ 继续提供“会话/异常分类”两个视图,只增加“根因项目”多选。异常分类汇总和问题行的项目标签都来自具体异常的根因归属;不出现“角色”“改进项目”控件,不新增按修改项目筛选或统计的入口。新增的工程名称/别名搜索索引也只取根因关联,通用标题等文本搜索沿用现有行为。沿用现有组件、样式与离线页面,不新增 UI 依赖。
154
156
 
155
157
  ```text
156
158
  根因项目:[APE ×] [OT ×] 来源:[全部 ▾]
@@ -161,17 +163,17 @@ ISSUE-001 ……异常标题……
161
163
  ```
162
164
 
163
165
  - 异常抽屉中的每条问题只显示根因项目标签;项目较多时展示前几个和 `+N`,展开可看全。已获支持的根因直接显示项目名,候选根因附“待确认”,不额外排列内部类型与证据状态标签。
164
- - 分类卡片显示命中问题的主要根因项目及去重问题数;会话卡片显示其问题根因项目的并集。拟修改或实际修改的其他项目不混入这些标签与统计。
166
+ - 分类卡片显示“该类异常涉及的根因项目”及去重问题数;会话卡片仅显示会话事实、异常数量与进度,不展示项目归属。拟修改或实际修改的其他项目不混入这些标签与统计。
165
167
  - 多选根因项目使用 OR,与来源、搜索和进度使用 AND。候选根因也可被其项目筛选命中,但始终标明待确认。仅有“APE 根因、OT 修改”的问题只能被 APE 项目筛选命中;选 OT 不能因为修改位置在 OT 就命中。
166
168
  - 层级筛选默认包含所选工程及其子工程,并显示“含子项目”。父级用于筛选分组,不向 Issue 自动补写父级归因。
167
169
  - 保留“待归因”入口,涵盖尚未记录根因项目的问题,包括已有修复建议但根因仍未知的问题。已证实原因在登记工程以外时,在详情明确说明,不能以修改项目补齐归属。没有问题的会话在未筛选视图继续可见,不推断其根因工程。
168
170
  - 当前筛选的问题数、会话数和列表保持一致。进度图例按当前根因项目/来源/搜索范围计算各进度数量,点击图例再过滤列表,避免选中一档后其他档消失。
169
171
  - 计数以当前归档内的会话身份加 Issue ID 去重。一条异常命中多个根因项目也只计一次;各工程分项之和可以大于总问题数,不能直接相加为总计。内部存在改进关联不影响项目统计。
170
- - 根因项目选择与现有抽屉定位写入 URL hash,例如 `#view=category&project=asset-protocol-engine`;多个根因项目用重复参数。切换视图保留筛选,已有会话/分类深链继续可用,不引入角色参数。
172
+ - 根因项目选择与现有抽屉定位写入 URL hash,例如 `#view=category&project=asset-protocol-engine`;多个根因项目用重复参数。异常视图保留项目筛选,切回会话总览不受项目条件影响;旧的会话项目筛选链接转入异常视图,明确指定会话的链接保留会话定位,不引入角色参数。
171
173
 
172
174
  ### 会话页
173
175
 
174
- - 顶部显示“根因项目”,从本会话 Issue 的根因归属派生,避免新增一份可独立编辑的会话关联表;没有记录根因项目时显示“待归因”。
176
+ - 会话概览只显示会话事实;根因项目标签与筛选仅位于内部异常列表,不影响原始轨迹与证据。
175
177
  - 左侧问题卡片与问题详情摘要使用同一套根因项目标签,详情可查看归因依据和源码入口,不并列展示一份“改进项目”清单。
176
178
  - 修复方案及实际改动沿用已有阶段摘要,按项目说明拟修改/实际修改的位置和范围。修改就在根因项目内时正常展示文件与改动,不重复强调两种项目身份;修改在其他项目时写清“在 OT 的某个 Skill 修改,因为……”,同时说明该方案的作用与限制。提议与已执行改动保持区分。
177
179
  - 诊断记录展示自己的历史项目快照,明确区别于 Issue 当前关联,不把旧诊断显示成最新判断。
@@ -218,12 +220,12 @@ ISSUE-001 ……异常标题……
218
220
  | --- | --- | --- |
219
221
  | 1. 工程目录和关联记录 | 新增 `traces/projects.py`;修改 `cli/trace.py`、`traces/records.py`、`traces/archive.py` | 复用现有路径/锁/原子写/revision 工具;增加 `projects.json` 保留文件名,防止 Session ID 与其冲突;诊断快照及并发核对、修复/验证摘要的项目引用同步覆盖新字段 |
220
222
  | 2. Skill 流程与契约 | init、detect、diagnose、optimize、verify、visualize-trace;`docs/trace-tuning.md` 及中英文 README | 初始化阅读既有规则与名称、按项目优先诊断、按项目明确调查/修改范围、记录更新、跨仓候选与加载证据、页面刷新;capture 的来源身份与采集逻辑无须扩张 |
221
- | 3. 展示与回归 | `visualize/trace.py`、`visualize/traces.py`、两套页面 assets 和相关 tests | 共享根因项目读取/校验及命名结果;根因标签、筛选、去重、深链、历史快照、修复详情中的修改范围与取舍、过期提示与可访问性一起验收,不增加角色或修改项目筛选 |
223
+ | 3. 展示与回归 | `visualize/trace.py`、`visualize/traces.py`、两套页面 assets 和相关 tests | 共享根因项目读取/校验及命名结果;异常根因标签、筛选、去重、深链、历史快照、修复详情中的修改范围与取舍、过期提示与可访问性一起验收,不增加角色或修改项目筛选 |
222
224
 
223
225
  关键验收场景:
224
226
 
225
227
  1. 普通单仓、显式普通目录、多 submodule 和嵌套 submodule 均可登记;缺失 checkout 仍可展示边界。重跑保留人工字段,路径/别名歧义可见,父级循环及越界路径拒绝。
226
- 2. 同一 Issue 根因在 APE、可在 APE 或 OT 修改:大盘和会话概览只显示 APE 根因标签,APE 项目筛选命中、OT 项目筛选不命中。只有 OT 也有独立的归因依据时才增加其根因标签,待确认者必须明确标注;命中多个根因或父级时不重复计数。只有修改建议而无根因归属时仍显示待归因。
228
+ 2. 同一 Issue 根因在 APE、可在 APE 或 OT 修改:大盘异常视图和会话内异常列表只显示 APE 根因标签,APE 项目筛选命中、OT 项目筛选不命中。只有 OT 也有独立的归因依据时才增加其根因标签,待确认者必须明确标注;命中多个根因或父级时不重复计数。只有修改建议而无根因归属时仍显示待归因。
227
229
  3. 指定 APE/OT 后,若证据指向其他登记工程,调查继续并保存依据;仅出现工程名、调用传播或当前源码缺陷不足以确认历史根因。
228
230
  4. 修订关联保留 history;省略保留、空数组清空;已保存诊断和采用的哈希不变;并发 revision 冲突不能错配正文与关联。
229
231
  5. 未关联、停用/改名/移动工程、目录损坏、零问题会话及旧归档仍可浏览。新保留文件名不会与新会话目录冲突,旧同名会话仍可读取并报告迁移缺口;源码链接维持边界检查。