agentic-project-kit 1.0.1__py3-none-any.whl

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 (310) hide show
  1. agentic_project_kit/__init__.py +1 -0
  2. agentic_project_kit/absolute_path_portability_audit.py +241 -0
  3. agentic_project_kit/access_level_policy.py +14 -0
  4. agentic_project_kit/access_levels.py +22 -0
  5. agentic_project_kit/action_registry.py +195 -0
  6. agentic_project_kit/action_specs.py +286 -0
  7. agentic_project_kit/agent_command_runner.py +516 -0
  8. agentic_project_kit/audit_test_matrix.py +36 -0
  9. agentic_project_kit/audit_test_matrix_cli.py +43 -0
  10. agentic_project_kit/chat_bootloader.py +257 -0
  11. agentic_project_kit/chat_entrypoint_contract.py +297 -0
  12. agentic_project_kit/checks.py +511 -0
  13. agentic_project_kit/ci_readiness.py +211 -0
  14. agentic_project_kit/cli.py +147 -0
  15. agentic_project_kit/cli_commands/__init__.py +1 -0
  16. agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
  17. agentic_project_kit/cli_commands/actions.py +21 -0
  18. agentic_project_kit/cli_commands/artifact_gc.py +240 -0
  19. agentic_project_kit/cli_commands/boot.py +55 -0
  20. agentic_project_kit/cli_commands/chat.py +34 -0
  21. agentic_project_kit/cli_commands/checks.py +305 -0
  22. agentic_project_kit/cli_commands/cockpit.py +103 -0
  23. agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
  24. agentic_project_kit/cli_commands/commands.py +129 -0
  25. agentic_project_kit/cli_commands/dev.py +23 -0
  26. agentic_project_kit/cli_commands/direction.py +92 -0
  27. agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
  28. agentic_project_kit/cli_commands/doc_registry.py +126 -0
  29. agentic_project_kit/cli_commands/docs.py +223 -0
  30. agentic_project_kit/cli_commands/dpa.py +1338 -0
  31. agentic_project_kit/cli_commands/evidence.py +251 -0
  32. agentic_project_kit/cli_commands/github.py +34 -0
  33. agentic_project_kit/cli_commands/governance.py +14 -0
  34. agentic_project_kit/cli_commands/gui.py +23 -0
  35. agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
  36. agentic_project_kit/cli_commands/handoff.py +183 -0
  37. agentic_project_kit/cli_commands/human_workflows.py +430 -0
  38. agentic_project_kit/cli_commands/init.py +132 -0
  39. agentic_project_kit/cli_commands/instruction.py +75 -0
  40. agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
  41. agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
  42. agentic_project_kit/cli_commands/pass_already_done.py +66 -0
  43. agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
  44. agentic_project_kit/cli_commands/patch_preflight.py +37 -0
  45. agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
  46. agentic_project_kit/cli_commands/patterns.py +40 -0
  47. agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
  48. agentic_project_kit/cli_commands/pr.py +133 -0
  49. agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
  50. agentic_project_kit/cli_commands/profiles.py +26 -0
  51. agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
  52. agentic_project_kit/cli_commands/project_direction.py +25 -0
  53. agentic_project_kit/cli_commands/release.py +392 -0
  54. agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
  55. agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
  56. agentic_project_kit/cli_commands/remote_next.py +57 -0
  57. agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
  58. agentic_project_kit/cli_commands/rule_registry.py +129 -0
  59. agentic_project_kit/cli_commands/rules.py +222 -0
  60. agentic_project_kit/cli_commands/scaffold.py +46 -0
  61. agentic_project_kit/cli_commands/slice.py +21 -0
  62. agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
  63. agentic_project_kit/cli_commands/state.py +51 -0
  64. agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
  65. agentic_project_kit/cli_commands/todo.py +53 -0
  66. agentic_project_kit/cli_commands/transfer.py +19 -0
  67. agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
  68. agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
  69. agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
  70. agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
  71. agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
  72. agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
  73. agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
  74. agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
  75. agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
  76. agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
  77. agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
  78. agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
  79. agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
  80. agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
  81. agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
  82. agentic_project_kit/cli_commands/transfer_shared.py +274 -0
  83. agentic_project_kit/cli_commands/validation.py +200 -0
  84. agentic_project_kit/cli_commands/version.py +27 -0
  85. agentic_project_kit/cli_commands/work_orders.py +219 -0
  86. agentic_project_kit/cli_commands/workflow.py +471 -0
  87. agentic_project_kit/cli_commands/workflow_guard.py +26 -0
  88. agentic_project_kit/cli_commands/workspace.py +252 -0
  89. agentic_project_kit/cockpit.py +608 -0
  90. agentic_project_kit/cockpit_readiness.py +67 -0
  91. agentic_project_kit/command_authority_audit.py +192 -0
  92. agentic_project_kit/command_inbox_check.py +103 -0
  93. agentic_project_kit/command_manifest.py +591 -0
  94. agentic_project_kit/command_queue_contract.py +161 -0
  95. agentic_project_kit/command_selector.py +245 -0
  96. agentic_project_kit/command_taxonomy.py +264 -0
  97. agentic_project_kit/commit_guard.py +67 -0
  98. agentic_project_kit/communication_artifact_gc.py +495 -0
  99. agentic_project_kit/communication_rule_context.py +437 -0
  100. agentic_project_kit/communication_state.py +115 -0
  101. agentic_project_kit/composed_wrapper_invariants.py +97 -0
  102. agentic_project_kit/composite_short_circuit.py +140 -0
  103. agentic_project_kit/contract.py +251 -0
  104. agentic_project_kit/control_state.py +83 -0
  105. agentic_project_kit/doc_currency_audit.py +536 -0
  106. agentic_project_kit/doc_lifecycle.py +1179 -0
  107. agentic_project_kit/doc_lifecycle_signals.py +446 -0
  108. agentic_project_kit/doc_lifecycle_sweep.py +639 -0
  109. agentic_project_kit/doc_mesh.py +491 -0
  110. agentic_project_kit/doctor.py +379 -0
  111. agentic_project_kit/documentation_registry.py +1122 -0
  112. agentic_project_kit/documentation_system_audit.py +310 -0
  113. agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
  114. agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
  115. agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
  116. agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
  117. agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
  118. agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
  119. agentic_project_kit/dpa_final_closeout.py +627 -0
  120. agentic_project_kit/dpa_fixture_evidence.py +680 -0
  121. agentic_project_kit/dpa_maintainer_record_check.py +711 -0
  122. agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
  123. agentic_project_kit/dpa_probe_002_readiness.py +509 -0
  124. agentic_project_kit/dpa_probe_003_readiness.py +417 -0
  125. agentic_project_kit/dpa_probe_004_readiness.py +437 -0
  126. agentic_project_kit/dpa_readiness.py +505 -0
  127. agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
  128. agentic_project_kit/dpa_registry_contracts.py +312 -0
  129. agentic_project_kit/dpa_renderer_readiness.py +407 -0
  130. agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
  131. agentic_project_kit/dpa_stable_readiness.py +743 -0
  132. agentic_project_kit/dpa_successor_projection.py +168 -0
  133. agentic_project_kit/dpa_workspace_init_projection.py +93 -0
  134. agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
  135. agentic_project_kit/entrypoint_slice_runner.py +153 -0
  136. agentic_project_kit/evidence_clean.py +152 -0
  137. agentic_project_kit/evidence_commit_paths.py +214 -0
  138. agentic_project_kit/evidence_finalize_log.py +204 -0
  139. agentic_project_kit/evidence_guard.py +114 -0
  140. agentic_project_kit/evidence_inspector.py +507 -0
  141. agentic_project_kit/evidence_state_contract.py +99 -0
  142. agentic_project_kit/execution_mode_state.py +148 -0
  143. agentic_project_kit/final_summary_contract.py +85 -0
  144. agentic_project_kit/finalize_guard.py +139 -0
  145. agentic_project_kit/gatekeeper_core.py +232 -0
  146. agentic_project_kit/github.py +25 -0
  147. agentic_project_kit/governance.py +59 -0
  148. agentic_project_kit/gui_action_execution.py +105 -0
  149. agentic_project_kit/gui_action_renderer.py +81 -0
  150. agentic_project_kit/gui_action_run_intent.py +98 -0
  151. agentic_project_kit/gui_action_views.py +195 -0
  152. agentic_project_kit/gui_activity_log.py +76 -0
  153. agentic_project_kit/gui_button_catalog.py +605 -0
  154. agentic_project_kit/gui_cockpit.py +365 -0
  155. agentic_project_kit/gui_cockpit_actions.py +695 -0
  156. agentic_project_kit/gui_cockpit_common.py +172 -0
  157. agentic_project_kit/gui_cockpit_header.py +501 -0
  158. agentic_project_kit/gui_cockpit_sidebar.py +387 -0
  159. agentic_project_kit/gui_cockpit_task.py +748 -0
  160. agentic_project_kit/gui_command_projection.py +355 -0
  161. agentic_project_kit/gui_communication_modes.py +130 -0
  162. agentic_project_kit/gui_dry_run.py +219 -0
  163. agentic_project_kit/gui_gatekeeper_status.py +259 -0
  164. agentic_project_kit/gui_layout_plan.py +148 -0
  165. agentic_project_kit/gui_open_folder.py +18 -0
  166. agentic_project_kit/gui_output_status_panel.py +61 -0
  167. agentic_project_kit/gui_panel_state.py +47 -0
  168. agentic_project_kit/gui_presenter.py +62 -0
  169. agentic_project_kit/gui_readiness_gate.py +206 -0
  170. agentic_project_kit/gui_release_flow.py +59 -0
  171. agentic_project_kit/gui_task_editor.py +1077 -0
  172. agentic_project_kit/gui_tk_widgets.py +177 -0
  173. agentic_project_kit/gui_tkinter_renderer.py +87 -0
  174. agentic_project_kit/gui_tkinter_shell.py +1176 -0
  175. agentic_project_kit/gui_transfer_contract.py +28 -0
  176. agentic_project_kit/gui_viewmodel.py +698 -0
  177. agentic_project_kit/gui_window_guard.py +46 -0
  178. agentic_project_kit/handoff_freshness.py +390 -0
  179. agentic_project_kit/handoff_prompt.py +180 -0
  180. agentic_project_kit/handoff_state.py +254 -0
  181. agentic_project_kit/instruction_lint.py +455 -0
  182. agentic_project_kit/llm_context_carriers.py +66 -0
  183. agentic_project_kit/llm_execution_context.py +285 -0
  184. agentic_project_kit/local_command_stack.py +133 -0
  185. agentic_project_kit/local_feature_gate.py +82 -0
  186. agentic_project_kit/local_garbage_collector.py +269 -0
  187. agentic_project_kit/models.py +18 -0
  188. agentic_project_kit/mutation_lock_audit.py +421 -0
  189. agentic_project_kit/next_turn_evidence.py +253 -0
  190. agentic_project_kit/next_turn_merge_if_green.py +459 -0
  191. agentic_project_kit/next_turn_pr_status.py +255 -0
  192. agentic_project_kit/next_turn_result.py +183 -0
  193. agentic_project_kit/next_turn_runner.py +159 -0
  194. agentic_project_kit/next_turn_slot.py +107 -0
  195. agentic_project_kit/next_turn_status.py +274 -0
  196. agentic_project_kit/ns_legacy_reference_audit.py +258 -0
  197. agentic_project_kit/ns_slice_runner.py +16 -0
  198. agentic_project_kit/operational_handoff_projection.py +124 -0
  199. agentic_project_kit/output_contract.py +80 -0
  200. agentic_project_kit/output_repair.py +61 -0
  201. agentic_project_kit/pass_already_done.py +216 -0
  202. agentic_project_kit/patch_artifact_preflight.py +147 -0
  203. agentic_project_kit/patch_cycle_workflow.py +440 -0
  204. agentic_project_kit/patch_failure_discipline_audit.py +223 -0
  205. agentic_project_kit/patch_preflight.py +215 -0
  206. agentic_project_kit/path_literal_audit.py +612 -0
  207. agentic_project_kit/patterns.py +99 -0
  208. agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
  209. agentic_project_kit/portability_shell_gate.py +40 -0
  210. agentic_project_kit/post_merge_handoff_refresh.py +56 -0
  211. agentic_project_kit/post_release.py +252 -0
  212. agentic_project_kit/post_release_closeout.py +616 -0
  213. agentic_project_kit/pr_cleanup.py +172 -0
  214. agentic_project_kit/pr_closeout.py +50 -0
  215. agentic_project_kit/pr_closeout_complete.py +344 -0
  216. agentic_project_kit/pr_create_or_skip.py +71 -0
  217. agentic_project_kit/pr_hygiene.py +210 -0
  218. agentic_project_kit/program_redundancy_audit.py +251 -0
  219. agentic_project_kit/project_direction.py +840 -0
  220. agentic_project_kit/protected_change_planner.py +194 -0
  221. agentic_project_kit/release.py +401 -0
  222. agentic_project_kit/release_doi_safety.py +24 -0
  223. agentic_project_kit/release_gate_core.py +174 -0
  224. agentic_project_kit/release_metadata_authority_gate.py +328 -0
  225. agentic_project_kit/release_metadata_prep.py +43 -0
  226. agentic_project_kit/release_notes.py +823 -0
  227. agentic_project_kit/release_prep_core.py +204 -0
  228. agentic_project_kit/release_prepare.py +369 -0
  229. agentic_project_kit/release_process_guardrails.py +247 -0
  230. agentic_project_kit/release_publish_core.py +114 -0
  231. agentic_project_kit/release_publish_orchestration.py +469 -0
  232. agentic_project_kit/release_state.py +552 -0
  233. agentic_project_kit/release_verify_core.py +175 -0
  234. agentic_project_kit/remote_branch_hygiene.py +223 -0
  235. agentic_project_kit/remote_next.py +89 -0
  236. agentic_project_kit/remote_next_closeout.py +101 -0
  237. agentic_project_kit/remote_next_order_contract.py +391 -0
  238. agentic_project_kit/removed_ns_commands.py +458 -0
  239. agentic_project_kit/removed_source_audit.py +225 -0
  240. agentic_project_kit/rendering.py +13 -0
  241. agentic_project_kit/repair_report.py +79 -0
  242. agentic_project_kit/repo_identity.py +59 -0
  243. agentic_project_kit/result_report_classifier.py +57 -0
  244. agentic_project_kit/rule_ack.py +105 -0
  245. agentic_project_kit/rule_preservation.py +58 -0
  246. agentic_project_kit/rule_refresh.py +174 -0
  247. agentic_project_kit/rule_registry_registration.py +322 -0
  248. agentic_project_kit/rule_registry_report.py +206 -0
  249. agentic_project_kit/rule_registry_validator.py +704 -0
  250. agentic_project_kit/rule_snapshot.py +98 -0
  251. agentic_project_kit/rule_source_validator.py +133 -0
  252. agentic_project_kit/run_summary.py +68 -0
  253. agentic_project_kit/run_summary_renderer.py +238 -0
  254. agentic_project_kit/runtime_validator.py +68 -0
  255. agentic_project_kit/safe_file_replace.py +35 -0
  256. agentic_project_kit/safe_patch.py +47 -0
  257. agentic_project_kit/safe_push.py +218 -0
  258. agentic_project_kit/safe_remove_diagnostic.py +51 -0
  259. agentic_project_kit/scaffold.py +69 -0
  260. agentic_project_kit/site_claims.py +444 -0
  261. agentic_project_kit/site_generator.py +1140 -0
  262. agentic_project_kit/slice_gate.py +225 -0
  263. agentic_project_kit/slice_runner.py +75 -0
  264. agentic_project_kit/standard_gates_audit_suite.py +167 -0
  265. agentic_project_kit/state_freshness.py +135 -0
  266. agentic_project_kit/status_current_state_audit.py +762 -0
  267. agentic_project_kit/successor_handoff_package.py +1763 -0
  268. agentic_project_kit/templates.py +292 -0
  269. agentic_project_kit/terminal_block_guard.py +166 -0
  270. agentic_project_kit/terminal_logging.py +275 -0
  271. agentic_project_kit/todo.py +97 -0
  272. agentic_project_kit/transfer_closeout.py +136 -0
  273. agentic_project_kit/transfer_continue.py +215 -0
  274. agentic_project_kit/transfer_local_runner.py +101 -0
  275. agentic_project_kit/transfer_operation_monitor.py +236 -0
  276. agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
  277. agentic_project_kit/transfer_post_merge_settle.py +383 -0
  278. agentic_project_kit/transfer_pr_actions.py +164 -0
  279. agentic_project_kit/transfer_remote_next.py +959 -0
  280. agentic_project_kit/transfer_repo_actions.py +2253 -0
  281. agentic_project_kit/transfer_runner.py +347 -0
  282. agentic_project_kit/transfer_safety_context.py +291 -0
  283. agentic_project_kit/transfer_state.py +483 -0
  284. agentic_project_kit/transfer_uplink.py +471 -0
  285. agentic_project_kit/transfer_workflow_next.py +148 -0
  286. agentic_project_kit/typed_work_order_evidence.py +58 -0
  287. agentic_project_kit/typed_work_order_queue.py +166 -0
  288. agentic_project_kit/typed_work_order_runner.py +168 -0
  289. agentic_project_kit/work_cycle.py +317 -0
  290. agentic_project_kit/work_discard_changes.py +129 -0
  291. agentic_project_kit/work_order_patch.py +199 -0
  292. agentic_project_kit/work_order_runner.py +120 -0
  293. agentic_project_kit/work_order_uploader.py +220 -0
  294. agentic_project_kit/work_order_validator.py +185 -0
  295. agentic_project_kit/work_orders.py +246 -0
  296. agentic_project_kit/workflow_guard.py +262 -0
  297. agentic_project_kit/workflow_summary_runner.py +124 -0
  298. agentic_project_kit/workspace.py +665 -0
  299. agentic_project_kit/workspace_adopt.py +471 -0
  300. agentic_project_kit/workspace_dpa_intake.py +406 -0
  301. agentic_project_kit/workspace_init.py +487 -0
  302. agentic_project_kit/workspace_lock.py +129 -0
  303. agentic_project_kit/workspace_remove.py +381 -0
  304. agentic_project_kit/workspace_upgrade.py +324 -0
  305. agentic_project_kit/wrapper_live_status.py +104 -0
  306. agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
  307. agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
  308. agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
  309. agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
  310. agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
@@ -0,0 +1,446 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date
5
+ import json
6
+ from pathlib import Path
7
+ import re
8
+ import subprocess
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from agentic_project_kit.documentation_registry import (
14
+ REGISTRY_PATH,
15
+ load_documentation_registry,
16
+ load_documentation_registry_scope,
17
+ )
18
+ from agentic_project_kit.release import read_project_version
19
+ from agentic_project_kit.state_freshness import _version_tuple
20
+
21
+ REVIEW_AFTER_RE = re.compile(r"^(date|release|direction):(.+)$")
22
+ RELEASE_REVIEW_RE = re.compile(r"^(<=|>=|==|<|>)(v?\d+\.\d+\.\d+)$")
23
+ TARGET_VERSION_RE = re.compile(r"\b(?:Target|target|for|For)\s+v?(\d+\.\d+\.\d+)\b")
24
+ REFERENCE_SCAN_SUFFIXES = {".md", ".py", ".yaml", ".yml"}
25
+ ORPHAN_EXEMPT_PREFIXES = ("docs/archive/", "docs/reports/", "docs/examples/")
26
+ ROOT_CANONICAL_FILES = ("README.md", "CHANGELOG.md", "CITATION.cff", "SECURITY.md")
27
+ PROJECT_DIRECTION_PAIR = (
28
+ "docs/planning/PROJECT_DIRECTION.yaml",
29
+ "docs/planning/PROJECT_DIRECTION.md",
30
+ )
31
+ REFERENCE_SCAN_EXCLUDED_PARTS = {".git", ".venv", "node_modules", "__pycache__"}
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class LifecycleSignal:
36
+ code: str
37
+ path: str
38
+ message: str
39
+ severity: str = "WARN"
40
+ document_class: str | None = None
41
+
42
+ def to_finding_kwargs(self) -> dict[str, Any]:
43
+ return {
44
+ "code": self.code,
45
+ "path": self.path,
46
+ "message": self.message,
47
+ "severity": self.severity,
48
+ "document_class": self.document_class,
49
+ }
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class DocOrphanCandidate:
54
+ path: str
55
+ document_class: str | None
56
+ last_commit_date: str | None
57
+
58
+ def to_dict(self) -> dict[str, Any]:
59
+ return {
60
+ "path": self.path,
61
+ "class": self.document_class,
62
+ "last_commit_date": self.last_commit_date,
63
+ }
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class DocOrphanReport:
68
+ candidates: tuple[DocOrphanCandidate, ...]
69
+
70
+ @property
71
+ def ok(self) -> bool:
72
+ return True
73
+
74
+ def to_dict(self) -> dict[str, Any]:
75
+ return {
76
+ "schema_version": 1,
77
+ "kind": "doc_orphan_audit",
78
+ "ok": True,
79
+ "candidate_count": len(self.candidates),
80
+ "candidates": [candidate.to_dict() for candidate in self.candidates],
81
+ }
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ReviewAfterSuggestion:
86
+ path: str
87
+ review_after: str
88
+ matched_text: str
89
+
90
+ def to_dict(self) -> dict[str, str]:
91
+ return {
92
+ "path": self.path,
93
+ "review_after": self.review_after,
94
+ "matched_text": self.matched_text,
95
+ }
96
+
97
+
98
+ def build_review_after_findings(
99
+ *,
100
+ path: str,
101
+ review_after: Any,
102
+ now: date,
103
+ current_version: str | None,
104
+ direction_statuses: dict[str, str],
105
+ document_class: str | None,
106
+ ) -> tuple[LifecycleSignal, ...]:
107
+ if review_after is None:
108
+ return ()
109
+ if not isinstance(review_after, str) or not review_after.strip():
110
+ return (
111
+ LifecycleSignal(
112
+ "REVIEW_AFTER_INVALID",
113
+ path,
114
+ "review_after must be a non-empty string",
115
+ document_class=document_class,
116
+ ),
117
+ )
118
+
119
+ value = review_after.strip()
120
+ match = REVIEW_AFTER_RE.fullmatch(value)
121
+ if not match:
122
+ return (
123
+ LifecycleSignal(
124
+ "REVIEW_AFTER_INVALID",
125
+ path,
126
+ f"unsupported review_after value: {value!r}",
127
+ document_class=document_class,
128
+ ),
129
+ )
130
+
131
+ kind, payload = match.groups()
132
+ if kind == "date":
133
+ return _date_review_findings(path, payload, now, document_class)
134
+ if kind == "release":
135
+ return _release_review_findings(path, payload, current_version, document_class)
136
+ if kind == "direction":
137
+ return _direction_review_findings(path, payload, direction_statuses, document_class)
138
+ return ()
139
+
140
+
141
+ def load_direction_statuses(project_root: Path) -> dict[str, str]:
142
+ path = project_root / "docs" / "planning" / "PROJECT_DIRECTION.yaml"
143
+ if not path.exists():
144
+ return {}
145
+ try:
146
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
147
+ except (OSError, yaml.YAMLError):
148
+ return {}
149
+ if not isinstance(data, dict):
150
+ return {}
151
+
152
+ statuses: dict[str, str] = {}
153
+ for section in ("strategy", "roadmap", "plans", "ideas", "done", "discarded"):
154
+ items = data.get(section)
155
+ if not isinstance(items, list):
156
+ continue
157
+ for item in items:
158
+ if not isinstance(item, dict):
159
+ continue
160
+ item_id = item.get("id")
161
+ if not isinstance(item_id, str) or not item_id:
162
+ continue
163
+ if section in {"done", "discarded"}:
164
+ statuses[item_id] = section
165
+ else:
166
+ status = item.get("status")
167
+ if isinstance(status, str) and status:
168
+ statuses[item_id] = status
169
+ return statuses
170
+
171
+
172
+ def resolve_current_version(project_root: Path, explicit_version: str | None = None) -> str | None:
173
+ if explicit_version:
174
+ return explicit_version
175
+ try:
176
+ return read_project_version(project_root)
177
+ except (OSError, ValueError):
178
+ return None
179
+
180
+
181
+ def build_doc_orphan_report(project_root: Path) -> DocOrphanReport:
182
+ registry = load_documentation_registry(project_root)
183
+ documents = registry.get("documents", [])
184
+ if not isinstance(documents, list):
185
+ return DocOrphanReport(candidates=())
186
+
187
+ candidates: list[DocOrphanCandidate] = []
188
+ reference_texts = _reference_scan_texts(project_root)
189
+ for entry in documents:
190
+ if not isinstance(entry, dict):
191
+ continue
192
+ path = entry.get("path")
193
+ if not isinstance(path, str) or not path:
194
+ continue
195
+ if _is_orphan_exempt(project_root, path):
196
+ continue
197
+ document_path = project_root / path
198
+ if not document_path.exists() or not document_path.is_file():
199
+ continue
200
+ if any(path in text for relative, text in reference_texts.items() if relative != path):
201
+ continue
202
+ candidates.append(
203
+ DocOrphanCandidate(
204
+ path=path,
205
+ document_class=entry.get("class") if isinstance(entry.get("class"), str) else None,
206
+ last_commit_date=_last_commit_date(project_root, path),
207
+ )
208
+ )
209
+ return DocOrphanReport(candidates=tuple(sorted(candidates, key=lambda candidate: candidate.path)))
210
+
211
+
212
+ def render_doc_orphan_report(report: DocOrphanReport) -> str:
213
+ lines = [
214
+ "DOC_ORPHAN_AUDIT",
215
+ "STATUS=PASS",
216
+ f"CANDIDATE_COUNT={len(report.candidates)}",
217
+ ]
218
+ if not report.candidates:
219
+ lines.append("CANDIDATE=none")
220
+ for candidate in report.candidates:
221
+ class_suffix = f"|class={candidate.document_class}" if candidate.document_class else ""
222
+ date_suffix = f"|last_commit_date={candidate.last_commit_date}" if candidate.last_commit_date else ""
223
+ lines.append(f"CANDIDATE={candidate.path}{class_suffix}{date_suffix}")
224
+ return "\n".join(lines) + "\n"
225
+
226
+
227
+ def write_doc_orphan_json_report(report: DocOrphanReport, output_path: Path) -> None:
228
+ output_path.parent.mkdir(parents=True, exist_ok=True)
229
+ output_path.write_text(
230
+ json.dumps(report.to_dict(), indent=2, sort_keys=True) + "\n",
231
+ encoding="utf-8",
232
+ )
233
+
234
+
235
+ def build_review_after_suggestions(project_root: Path) -> tuple[ReviewAfterSuggestion, ...]:
236
+ registry = load_documentation_registry(project_root)
237
+ documents = registry.get("documents", [])
238
+ if not isinstance(documents, list):
239
+ return ()
240
+
241
+ suggestions: list[ReviewAfterSuggestion] = []
242
+ for entry in documents:
243
+ if not isinstance(entry, dict) or entry.get("review_after") is not None:
244
+ continue
245
+ path = entry.get("path")
246
+ if not isinstance(path, str) or not path.endswith(".md"):
247
+ continue
248
+ document_path = project_root / path
249
+ if not document_path.exists() or not document_path.is_file():
250
+ continue
251
+ try:
252
+ text = document_path.read_text(encoding="utf-8")
253
+ except OSError:
254
+ continue
255
+ match = TARGET_VERSION_RE.search(text)
256
+ if match:
257
+ suggestions.append(
258
+ ReviewAfterSuggestion(
259
+ path=path,
260
+ review_after=f"release:>={match.group(1)}",
261
+ matched_text=match.group(0),
262
+ )
263
+ )
264
+ return tuple(sorted(suggestions, key=lambda suggestion: suggestion.path))
265
+
266
+
267
+ def render_review_after_suggestions(suggestions: tuple[ReviewAfterSuggestion, ...]) -> str:
268
+ lines = ["REVIEW_AFTER_SUGGESTIONS", f"COUNT={len(suggestions)}"]
269
+ if not suggestions:
270
+ lines.append("SUGGESTION=none")
271
+ for suggestion in suggestions:
272
+ lines.append(f"SUGGESTION={suggestion.path}|{suggestion.review_after}|matched={suggestion.matched_text}")
273
+ return "\n".join(lines) + "\n"
274
+
275
+
276
+ def review_after_suggestions_to_dict(suggestions: tuple[ReviewAfterSuggestion, ...]) -> dict[str, Any]:
277
+ return {
278
+ "schema_version": 1,
279
+ "kind": "review_after_suggestions",
280
+ "count": len(suggestions),
281
+ "suggestions": [suggestion.to_dict() for suggestion in suggestions],
282
+ }
283
+
284
+
285
+ def _date_review_findings(
286
+ path: str,
287
+ payload: str,
288
+ now: date,
289
+ document_class: str | None,
290
+ ) -> tuple[LifecycleSignal, ...]:
291
+ try:
292
+ target = date.fromisoformat(payload)
293
+ except ValueError:
294
+ return (
295
+ LifecycleSignal(
296
+ "REVIEW_AFTER_INVALID",
297
+ path,
298
+ f"invalid review_after date: {payload!r}",
299
+ document_class=document_class,
300
+ ),
301
+ )
302
+ if now >= target:
303
+ return (
304
+ LifecycleSignal(
305
+ "REVIEW_DUE_DATE",
306
+ path,
307
+ f"review_after date is due: {target.isoformat()}",
308
+ document_class=document_class,
309
+ ),
310
+ )
311
+ return ()
312
+
313
+
314
+ def _release_review_findings(
315
+ path: str,
316
+ payload: str,
317
+ current_version: str | None,
318
+ document_class: str | None,
319
+ ) -> tuple[LifecycleSignal, ...]:
320
+ match = RELEASE_REVIEW_RE.fullmatch(payload)
321
+ if not match:
322
+ return (
323
+ LifecycleSignal(
324
+ "REVIEW_AFTER_INVALID",
325
+ path,
326
+ f"invalid review_after release selector: {payload!r}",
327
+ document_class=document_class,
328
+ ),
329
+ )
330
+ if current_version is None:
331
+ return (
332
+ LifecycleSignal(
333
+ "REVIEW_AFTER_INVALID",
334
+ path,
335
+ "review_after release selector requires a readable current project version",
336
+ document_class=document_class,
337
+ ),
338
+ )
339
+
340
+ operator, target_version = match.groups()
341
+ if _version_due(current_version, operator, target_version):
342
+ return (
343
+ LifecycleSignal(
344
+ "REVIEW_DUE_RELEASE",
345
+ path,
346
+ f"review_after release selector is due: current {current_version} {operator} {target_version}",
347
+ document_class=document_class,
348
+ ),
349
+ )
350
+ return ()
351
+
352
+
353
+ def _direction_review_findings(
354
+ path: str,
355
+ item_id: str,
356
+ direction_statuses: dict[str, str],
357
+ document_class: str | None,
358
+ ) -> tuple[LifecycleSignal, ...]:
359
+ item_id = item_id.strip()
360
+ if not item_id:
361
+ return (
362
+ LifecycleSignal(
363
+ "REVIEW_AFTER_INVALID",
364
+ path,
365
+ "review_after direction selector requires an item id",
366
+ document_class=document_class,
367
+ ),
368
+ )
369
+ status = direction_statuses.get(item_id)
370
+ if status is None:
371
+ return (
372
+ LifecycleSignal(
373
+ "REVIEW_AFTER_INVALID",
374
+ path,
375
+ f"review_after direction selector references unknown item: {item_id}",
376
+ document_class=document_class,
377
+ ),
378
+ )
379
+ if status in {"done", "discarded"}:
380
+ return (
381
+ LifecycleSignal(
382
+ "REVIEW_DUE_DIRECTION",
383
+ path,
384
+ f"review_after direction selector is due: {item_id} is {status}",
385
+ document_class=document_class,
386
+ ),
387
+ )
388
+ return ()
389
+
390
+
391
+ def _version_due(current_version: str, operator: str, target_version: str) -> bool:
392
+ current = _version_tuple(current_version.removeprefix("v"))
393
+ target = _version_tuple(target_version.removeprefix("v"))
394
+ if current is None or target is None:
395
+ return False
396
+ if operator == "<":
397
+ return current < target
398
+ if operator == "<=":
399
+ return current <= target
400
+ if operator == "==":
401
+ return current == target
402
+ if operator == ">=":
403
+ return current >= target
404
+ if operator == ">":
405
+ return current > target
406
+ return False
407
+
408
+
409
+ def _reference_scan_texts(project_root: Path) -> dict[str, str]:
410
+ texts: dict[str, str] = {}
411
+ for path in project_root.rglob("*"):
412
+ if not path.is_file() or path.suffix not in REFERENCE_SCAN_SUFFIXES:
413
+ continue
414
+ if REFERENCE_SCAN_EXCLUDED_PARTS & set(path.parts):
415
+ continue
416
+ relative = path.relative_to(project_root).as_posix()
417
+ if relative == REGISTRY_PATH.as_posix():
418
+ continue
419
+ try:
420
+ texts[relative] = path.read_text(encoding="utf-8")
421
+ except (OSError, UnicodeDecodeError):
422
+ continue
423
+ return texts
424
+
425
+
426
+ def _is_orphan_exempt(project_root: Path, path: str) -> bool:
427
+ if path in ROOT_CANONICAL_FILES or path in PROJECT_DIRECTION_PAIR:
428
+ return True
429
+ if any(path.startswith(prefix) for prefix in ORPHAN_EXEMPT_PREFIXES):
430
+ return True
431
+ scope = load_documentation_registry_scope(project_root)
432
+ if path in scope.required_files:
433
+ return True
434
+ return False
435
+
436
+
437
+ def _last_commit_date(project_root: Path, path: str) -> str | None:
438
+ result = subprocess.run(
439
+ ["git", "log", "-1", "--format=%cI", "--", path],
440
+ cwd=project_root,
441
+ check=False,
442
+ capture_output=True,
443
+ text=True,
444
+ )
445
+ value = result.stdout.strip()
446
+ return value or None