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,458 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import re
6
+ import subprocess
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Iterable, Sequence
11
+
12
+
13
+ TEXT_SUFFIXES = {
14
+ "",
15
+ ".py",
16
+ ".sh",
17
+ ".bash",
18
+ ".zsh",
19
+ ".md",
20
+ ".rst",
21
+ ".txt",
22
+ ".toml",
23
+ ".yaml",
24
+ ".yml",
25
+ ".json",
26
+ ".ini",
27
+ ".cfg",
28
+ ".conf",
29
+ ".jinja",
30
+ ".j2",
31
+ }
32
+
33
+ BINARY_SUFFIXES = {
34
+ ".png",
35
+ ".jpg",
36
+ ".jpeg",
37
+ ".gif",
38
+ ".pdf",
39
+ ".zip",
40
+ ".gz",
41
+ ".tgz",
42
+ ".pyc",
43
+ ".sqlite",
44
+ ".db",
45
+ ".woff",
46
+ ".woff2",
47
+ }
48
+
49
+ SEARCH_ROOTS = ("ns", "ns-menu", "tools", "src", "tests", "docs", ".agentic", "scripts", "bin")
50
+
51
+ EXCLUDED_PATH_PREFIXES = (
52
+ "docs/reports/",
53
+ "docs/reports_archive/",
54
+ ".agentic/transfer/inbox/",
55
+ ".agentic/transfer/outbox/",
56
+ )
57
+
58
+ EXCLUDED_PATHS = {
59
+ ".agentic/transfer/inbox/current.yaml",
60
+ ".agentic/transfer/inbox/next_command.py.txt",
61
+ ".agentic/transfer/outbox/last_result.txt",
62
+ }
63
+
64
+ NS_PATTERNS = (
65
+ r"\bns-dev\b",
66
+ r"\bns\s+dev\b",
67
+ r"\bns-go\b",
68
+ r"\bns\s+go\b",
69
+ r"\bns-[a-z0-9][a-z0-9_.-]*\b",
70
+ r"\bns\s+[a-z0-9][a-z0-9_.-]*(?:\s+[a-z0-9][a-z0-9_.-]*)?\b",
71
+ )
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class GitResult:
76
+ argv: list[str]
77
+ returncode: int
78
+ stdout: str
79
+ stderr: str
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class Hit:
84
+ ref: str
85
+ path: str
86
+ line_no: int
87
+ line: str
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class CommandCandidate:
92
+ name: str
93
+ evidence_count_old: int
94
+ evidence_count_new: int
95
+ evidence_count_main: int
96
+ removed_or_decreased_signal: bool
97
+
98
+
99
+ def _git(args: Sequence[str], *, check: bool = True) -> GitResult:
100
+ argv = ["git", *args]
101
+ cp = subprocess.run(
102
+ argv,
103
+ text=True,
104
+ stdout=subprocess.PIPE,
105
+ stderr=subprocess.PIPE,
106
+ check=False,
107
+ )
108
+ result = GitResult(argv=argv, returncode=cp.returncode, stdout=cp.stdout, stderr=cp.stderr)
109
+ if check and cp.returncode != 0:
110
+ raise RuntimeError(
111
+ "git command failed\n"
112
+ f"argv={argv!r}\n"
113
+ f"returncode={cp.returncode}\n"
114
+ f"stdout={cp.stdout}\n"
115
+ f"stderr={cp.stderr}"
116
+ )
117
+ return result
118
+
119
+
120
+ def _resolve_ref(ref: str) -> str:
121
+ candidates = [ref]
122
+ if not ref.startswith("v"):
123
+ candidates.append(f"v{ref}")
124
+ elif ref.startswith("v"):
125
+ candidates.append(ref[1:])
126
+
127
+ for candidate in dict.fromkeys(candidates):
128
+ result = _git(["rev-parse", "--verify", "--quiet", candidate], check=False)
129
+ if result.returncode == 0:
130
+ return candidate
131
+
132
+ tags = _git(["tag", "-l", f"*{ref.lstrip('v')}*"], check=False).stdout.strip()
133
+ raise RuntimeError(f"Could not resolve release ref {ref!r}. Matching tags: {tags}")
134
+
135
+
136
+ def _list_files(ref: str) -> list[str]:
137
+ result = _git(["ls-tree", "-r", "--name-only", ref])
138
+ return [line.strip() for line in result.stdout.splitlines() if line.strip()]
139
+
140
+
141
+ def _path_in_scope(path: str) -> bool:
142
+ normalized = path.strip("/")
143
+ if normalized in EXCLUDED_PATHS:
144
+ return False
145
+ if any(normalized.startswith(prefix) for prefix in EXCLUDED_PATH_PREFIXES):
146
+ return False
147
+ return any(normalized == root or normalized.startswith(root + "/") for root in SEARCH_ROOTS)
148
+
149
+
150
+ def _is_probably_text(path: str) -> bool:
151
+ if not _path_in_scope(path):
152
+ return False
153
+ suffix = Path(path).suffix.lower()
154
+ if suffix in BINARY_SUFFIXES:
155
+ return False
156
+ if suffix in TEXT_SUFFIXES:
157
+ return True
158
+ return Path(path).name in {"ns", "ns-menu"}
159
+
160
+
161
+ def _file_text(ref: str, path: str) -> str | None:
162
+ if not _is_probably_text(path):
163
+ return None
164
+ result = _git(["show", f"{ref}:{path}"], check=False)
165
+ if result.returncode != 0:
166
+ return None
167
+ return result.stdout
168
+
169
+
170
+ def _collect_ref_hits(ref: str) -> list[Hit]:
171
+ regexes = [re.compile(pattern, re.IGNORECASE) for pattern in NS_PATTERNS]
172
+ hits: list[Hit] = []
173
+ for path in _list_files(ref):
174
+ text = _file_text(ref, path)
175
+ if text is None:
176
+ continue
177
+ for line_no, line in enumerate(text.splitlines(), start=1):
178
+ stripped = line.strip()
179
+ if stripped and any(regex.search(stripped) for regex in regexes):
180
+ hits.append(Hit(ref=ref, path=path, line_no=line_no, line=stripped[:320]))
181
+ return hits
182
+
183
+
184
+ def _iter_main_text_paths() -> Iterable[Path]:
185
+ for root_name in SEARCH_ROOTS:
186
+ root = Path(root_name)
187
+ if not root.exists():
188
+ continue
189
+ if root.is_file():
190
+ if _is_probably_text(str(root)):
191
+ yield root
192
+ continue
193
+ for path in root.rglob("*"):
194
+ if path.is_file() and _is_probably_text(str(path)):
195
+ yield path
196
+
197
+
198
+ def _collect_main_hits() -> list[Hit]:
199
+ regexes = [re.compile(pattern, re.IGNORECASE) for pattern in NS_PATTERNS]
200
+ hits: list[Hit] = []
201
+ for path in _iter_main_text_paths():
202
+ try:
203
+ text = path.read_text(encoding="utf-8")
204
+ except UnicodeDecodeError:
205
+ continue
206
+ for line_no, line in enumerate(text.splitlines(), start=1):
207
+ stripped = line.strip()
208
+ if stripped and any(regex.search(stripped) for regex in regexes):
209
+ hits.append(Hit(ref="main-worktree", path=str(path), line_no=line_no, line=stripped[:320]))
210
+ return hits
211
+
212
+
213
+ def _strip_shell_suffix(name: str) -> str:
214
+ for suffix in (".sh", ".bash", ".zsh", ".py"):
215
+ if name.endswith(suffix):
216
+ return name[: -len(suffix)]
217
+ return name
218
+
219
+
220
+ def _normalize_command_names(hits: Iterable[Hit]) -> set[str]:
221
+ names: set[str] = set()
222
+ for hit in hits:
223
+ line = hit.line.lower()
224
+ for match in re.finditer(r"\bns-[a-z0-9][a-z0-9_.-]*\b", line):
225
+ names.add(_strip_shell_suffix(match.group(0)))
226
+
227
+ for match in re.finditer(r"\bns\s+[a-z0-9][a-z0-9_.-]*(?:\s+[a-z0-9][a-z0-9_.-]*)?\b", line):
228
+ normalized = " ".join(match.group(0).split())
229
+ names.add(normalized)
230
+ parts = normalized.split()
231
+ if len(parts) >= 2:
232
+ names.add(" ".join(parts[:2]))
233
+ return names
234
+
235
+
236
+ def _count_name(hits: Iterable[Hit], name: str) -> int:
237
+ if " " in name:
238
+ pattern = r"\b" + re.escape(name).replace(r"\ ", r"\s+") + r"\b"
239
+ else:
240
+ pattern = r"\b" + re.escape(name) + r"\b"
241
+ regex = re.compile(pattern, re.IGNORECASE)
242
+ return sum(1 for hit in hits if regex.search(hit.line))
243
+
244
+
245
+ def _removed_lines(old_ref: str, new_ref: str) -> list[str]:
246
+ result = _git(
247
+ [
248
+ "diff",
249
+ "--unified=0",
250
+ f"{old_ref}..{new_ref}",
251
+ "--",
252
+ *SEARCH_ROOTS,
253
+ ],
254
+ check=False,
255
+ )
256
+ regexes = [re.compile(pattern, re.IGNORECASE) for pattern in NS_PATTERNS]
257
+ lines: list[str] = []
258
+ current_path: str | None = None
259
+
260
+ for line in result.stdout.splitlines():
261
+ if line.startswith("diff --git "):
262
+ parts = line.split()
263
+ if len(parts) >= 4:
264
+ current_path = parts[2][2:] if parts[2].startswith("a/") else parts[2]
265
+ else:
266
+ current_path = None
267
+ continue
268
+
269
+ if current_path is not None and not _path_in_scope(current_path):
270
+ continue
271
+
272
+ if not line.startswith("-") or line.startswith("---"):
273
+ continue
274
+ if any(regex.search(line) for regex in regexes):
275
+ lines.append(line[:360])
276
+ return lines
277
+
278
+
279
+ def _deleted_files(old_ref: str, new_ref: str) -> list[str]:
280
+ result = _git(
281
+ [
282
+ "diff",
283
+ "--name-status",
284
+ "--diff-filter=D",
285
+ f"{old_ref}..{new_ref}",
286
+ "--",
287
+ *SEARCH_ROOTS,
288
+ ],
289
+ check=False,
290
+ )
291
+ filtered: list[str] = []
292
+ for line in result.stdout.splitlines():
293
+ if not line.strip():
294
+ continue
295
+ parts = line.split(maxsplit=1)
296
+ if len(parts) != 2:
297
+ continue
298
+ status, deleted_path = parts
299
+ if status == "D" and _path_in_scope(deleted_path):
300
+ filtered.append(line)
301
+ return filtered
302
+
303
+
304
+ def build_removed_ns_command_report(old: str, new: str) -> dict:
305
+ old_ref = _resolve_ref(old)
306
+ new_ref = _resolve_ref(new)
307
+
308
+ old_hits = _collect_ref_hits(old_ref)
309
+ new_hits = _collect_ref_hits(new_ref)
310
+ main_hits = _collect_main_hits()
311
+ removed = _removed_lines(old_ref, new_ref)
312
+
313
+ removed_hit_proxy = [Hit(ref="diff", path="diff", line_no=i + 1, line=line) for i, line in enumerate(removed)]
314
+
315
+ names = sorted(
316
+ _normalize_command_names(old_hits)
317
+ | _normalize_command_names(new_hits)
318
+ | _normalize_command_names(main_hits)
319
+ | _normalize_command_names(removed_hit_proxy)
320
+ )
321
+
322
+ candidates: list[CommandCandidate] = []
323
+ for name in names:
324
+ old_count = _count_name(old_hits, name)
325
+ new_count = _count_name(new_hits, name)
326
+ main_count = _count_name(main_hits, name)
327
+ removed_count = _count_name(removed_hit_proxy, name)
328
+ candidates.append(
329
+ CommandCandidate(
330
+ name=name,
331
+ evidence_count_old=old_count,
332
+ evidence_count_new=new_count,
333
+ evidence_count_main=main_count,
334
+ removed_or_decreased_signal=removed_count > 0 or old_count > new_count,
335
+ )
336
+ )
337
+
338
+ candidates.sort(
339
+ key=lambda item: (
340
+ not item.removed_or_decreased_signal,
341
+ -item.evidence_count_old,
342
+ item.name,
343
+ )
344
+ )
345
+
346
+ return {
347
+ "schema_version": 1,
348
+ "kind": "removed_ns_command_diagnosis",
349
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
350
+ "old_ref": old_ref,
351
+ "new_ref": new_ref,
352
+ "old_commit": _git(["rev-parse", old_ref]).stdout.strip(),
353
+ "new_commit": _git(["rev-parse", new_ref]).stdout.strip(),
354
+ "candidate_commands": [asdict(candidate) for candidate in candidates],
355
+ "deleted_files": _deleted_files(old_ref, new_ref),
356
+ "removed_lines": removed,
357
+ "old_hits": [asdict(hit) for hit in old_hits],
358
+ "new_hits": [asdict(hit) for hit in new_hits],
359
+ "main_hits": [asdict(hit) for hit in main_hits],
360
+ }
361
+
362
+
363
+ def write_markdown_report(report: dict, path: Path) -> None:
364
+ lines: list[str] = []
365
+ lines.append("# Removed ns command diagnosis")
366
+ lines.append("")
367
+ lines.append(f"- old_ref: `{report['old_ref']}` / `{report['old_commit']}`")
368
+ lines.append(f"- new_ref: `{report['new_ref']}` / `{report['new_commit']}`")
369
+ lines.append("")
370
+ lines.append("## Candidate commands")
371
+ lines.append("")
372
+ lines.append("| command | old hits | new hits | main hits | removed/decreased |")
373
+ lines.append("|---|---:|---:|---:|---|")
374
+ for item in report["candidate_commands"]:
375
+ if item["removed_or_decreased_signal"] or item["name"] in {"ns-dev", "ns dev", "ns-go", "ns go"}:
376
+ lines.append(
377
+ f"| `{item['name']}` | {item['evidence_count_old']} | "
378
+ f"{item['evidence_count_new']} | {item['evidence_count_main']} | "
379
+ f"{item['removed_or_decreased_signal']} |"
380
+ )
381
+ lines.append("")
382
+ lines.append("## Deleted files")
383
+ lines.append("")
384
+ for line in report["deleted_files"][:300]:
385
+ lines.append(f"- `{line}`")
386
+ if not report["deleted_files"]:
387
+ lines.append("- none")
388
+ lines.append("")
389
+ lines.append("## Removed ns lines")
390
+ lines.append("")
391
+ for line in report["removed_lines"][:300]:
392
+ lines.append(f"```text\n{line}\n```")
393
+ if not report["removed_lines"]:
394
+ lines.append("- none")
395
+ lines.append("")
396
+ lines.append("## Old release hits")
397
+ lines.append("")
398
+ for hit in report["old_hits"][:500]:
399
+ lines.append(f"- `{hit['path']}:{hit['line_no']}`: `{hit['line']}`")
400
+ lines.append("")
401
+ lines.append("## New release hits")
402
+ lines.append("")
403
+ for hit in report["new_hits"][:300]:
404
+ lines.append(f"- `{hit['path']}:{hit['line_no']}`: `{hit['line']}`")
405
+ lines.append("")
406
+ lines.append("## Current main hits")
407
+ lines.append("")
408
+ for hit in report["main_hits"][:300]:
409
+ lines.append(f"- `{hit['path']}:{hit['line_no']}`: `{hit['line']}`")
410
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
411
+
412
+
413
+ def main(argv: Sequence[str] | None = None) -> int:
414
+ parser = argparse.ArgumentParser(description="Diagnose ns commands removed or reduced between release refs.")
415
+ parser.add_argument("--old", default="0.4.6")
416
+ parser.add_argument("--new", default="0.4.8")
417
+ parser.add_argument("--json-out", default=None)
418
+ parser.add_argument("--md-out", default=None)
419
+ parser.add_argument("--json", action="store_true")
420
+ args = parser.parse_args(argv)
421
+
422
+ report = build_removed_ns_command_report(old=args.old, new=args.new)
423
+
424
+ if args.json_out:
425
+ Path(args.json_out).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
426
+ if args.md_out:
427
+ write_markdown_report(report, Path(args.md_out))
428
+
429
+ if args.json:
430
+ print(json.dumps(report, indent=2, ensure_ascii=False))
431
+ else:
432
+ print("REMOVED_NS_COMMAND_DIAGNOSIS")
433
+ print(f"old_ref={report['old_ref']}")
434
+ print(f"new_ref={report['new_ref']}")
435
+ print(f"old_commit={report['old_commit']}")
436
+ print(f"new_commit={report['new_commit']}")
437
+ print("candidate_commands:")
438
+ for item in report["candidate_commands"][:40]:
439
+ print(
440
+ "- {name}: old={old} new={new} main={main} removed_or_decreased={signal}".format(
441
+ name=item["name"],
442
+ old=item["evidence_count_old"],
443
+ new=item["evidence_count_new"],
444
+ main=item["evidence_count_main"],
445
+ signal=item["removed_or_decreased_signal"],
446
+ )
447
+ )
448
+ if args.json_out:
449
+ print(f"json_out={args.json_out}")
450
+ if args.md_out:
451
+ print(f"md_out={args.md_out}")
452
+ print("RESULT=REMOVED_NS_COMMAND_DIAGNOSIS_DONE")
453
+
454
+ return 0
455
+
456
+
457
+ if __name__ == "__main__":
458
+ raise SystemExit(main())
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ import re
6
+
7
+
8
+ DEFAULT_CENTRAL_TARGET = "docs/planning/PROJECT_DIRECTION.yaml"
9
+ DEFAULT_MANIFESTS = (
10
+ "docs/DOCUMENTATION_REGISTRY.yaml",
11
+ "docs/DOCUMENTATION_COVERAGE.yaml",
12
+ "docs/DOC_REGISTRY_SCOPE.yaml",
13
+ )
14
+ SCAN_SUFFIXES = {".md", ".py", ".yaml", ".yml"}
15
+ SCAN_ROOTS = ("docs", "src", "tests")
16
+ EXTRA_SCAN_FILES = ("README.md",)
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RemovedSourceFinding:
21
+ path: str
22
+ exists: bool
23
+ registered_refs: list[str] = field(default_factory=list)
24
+ source_file_style_refs: list[str] = field(default_factory=list)
25
+ live_refs: list[str] = field(default_factory=list)
26
+
27
+ @property
28
+ def ok(self) -> bool:
29
+ return (
30
+ not self.exists
31
+ and not self.registered_refs
32
+ and not self.source_file_style_refs
33
+ and not self.live_refs
34
+ )
35
+
36
+ def as_dict(self) -> dict[str, object]:
37
+ return {
38
+ "path": self.path,
39
+ "exists": self.exists,
40
+ "registered_refs": self.registered_refs,
41
+ "source_file_style_refs": self.source_file_style_refs,
42
+ "live_refs": self.live_refs,
43
+ "ok": self.ok,
44
+ }
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class RemovedSourceAuditResult:
49
+ result_status: str
50
+ central_target: str
51
+ audited_count: int
52
+ findings: list[RemovedSourceFinding]
53
+
54
+ @property
55
+ def ok(self) -> bool:
56
+ return self.result_status == "PASS"
57
+
58
+ @property
59
+ def returncode(self) -> int:
60
+ return 0 if self.ok else 1
61
+
62
+ def as_dict(self) -> dict[str, object]:
63
+ return {
64
+ "result_status": self.result_status,
65
+ "central_target": self.central_target,
66
+ "audited_count": self.audited_count,
67
+ "findings": [finding.as_dict() for finding in self.findings],
68
+ }
69
+
70
+
71
+ def _rel(path: Path, root: Path) -> str:
72
+ return path.resolve().relative_to(root.resolve()).as_posix()
73
+
74
+
75
+ def _iter_scan_files(root: Path) -> list[Path]:
76
+ files: list[Path] = []
77
+ for scan_root in SCAN_ROOTS:
78
+ base = root / scan_root
79
+ if not base.exists():
80
+ continue
81
+ for path in base.rglob("*"):
82
+ if not path.is_file():
83
+ continue
84
+ if "docs/reports" in path.as_posix():
85
+ continue
86
+ if path.suffix in SCAN_SUFFIXES:
87
+ files.append(path)
88
+ for extra in EXTRA_SCAN_FILES:
89
+ path = root / extra
90
+ if path.exists() and path.is_file():
91
+ files.append(path)
92
+ return sorted(set(files))
93
+
94
+
95
+ def _line_refs(path: Path, root: Path, needle: str) -> list[str]:
96
+ refs: list[str] = []
97
+ try:
98
+ lines = path.read_text(encoding="utf-8").splitlines()
99
+ except UnicodeDecodeError:
100
+ return refs
101
+ rel = _rel(path, root)
102
+ for lineno, line in enumerate(lines, start=1):
103
+ if needle in line:
104
+ refs.append(f"{rel}:{lineno}:{line.strip()}")
105
+ return refs
106
+
107
+
108
+ def _extract_removed_source_paths(root: Path, central_target: str) -> list[str]:
109
+ central_path = root / central_target
110
+ if not central_path.exists():
111
+ return []
112
+ text = central_path.read_text(encoding="utf-8")
113
+
114
+ # Intentionally dependency-free YAML-ish parser. It recognizes both
115
+ # centralization forms used in PROJECT_DIRECTION.yaml:
116
+ #
117
+ # removed_source:
118
+ # status: removed_source
119
+ # path: docs/old.md
120
+ #
121
+ # removed_sources:
122
+ # - path: docs/old-a.md
123
+ # status: removed_source
124
+ #
125
+ # A candidate path is counted only when a nearby local block contains
126
+ # status: removed_source. This keeps ordinary source_files entries out of
127
+ # the removed-source set while still supporting both key orders.
128
+ removed: list[str] = []
129
+ pattern = re.compile(
130
+ r"^\s*(?:-\s+)?path:\s+(docs/[^\s#]+)\s*$",
131
+ flags=re.MULTILINE,
132
+ )
133
+
134
+ for match in pattern.finditer(text):
135
+ candidate = match.group(1)
136
+ window_start = max(0, match.start() - 420)
137
+ window_end = min(len(text), match.end() + 420)
138
+ window = text[window_start:window_end]
139
+ if re.search(
140
+ r"(?m)^\s*status:\s*removed_source\s*$",
141
+ window,
142
+ ):
143
+ removed.append(candidate)
144
+
145
+ return sorted(set(removed))
146
+
147
+ def _source_file_style_refs(root: Path, central_target: str, removed_path: str) -> list[str]:
148
+ path = root / central_target
149
+ if not path.exists():
150
+ return []
151
+ refs: list[str] = []
152
+ lines = path.read_text(encoding="utf-8").splitlines()
153
+ pattern = re.compile(rf"^\s*-\s+{re.escape(removed_path)}\s*$")
154
+ for lineno, line in enumerate(lines, start=1):
155
+ if pattern.match(line):
156
+ refs.append(f"{central_target}:{lineno}:{line.strip()}")
157
+ return refs
158
+
159
+
160
+ def _manifest_refs(root: Path, removed_path: str, manifests: tuple[str, ...] = DEFAULT_MANIFESTS) -> list[str]:
161
+ refs: list[str] = []
162
+ for manifest in manifests:
163
+ path = root / manifest
164
+ if path.exists():
165
+ refs.extend(_line_refs(path, root, removed_path))
166
+ return refs
167
+
168
+
169
+ def _live_refs(root: Path, central_target: str, removed_path: str) -> list[str]:
170
+ refs: list[str] = []
171
+ for path in _iter_scan_files(root):
172
+ rel = _rel(path, root)
173
+ if rel == central_target:
174
+ continue
175
+ if rel.startswith("docs/reports/"):
176
+ continue
177
+ refs.extend(_line_refs(path, root, removed_path))
178
+ return refs
179
+
180
+
181
+ def build_removed_source_audit(
182
+ root: Path,
183
+ paths: list[str] | None = None,
184
+ central_target: str = DEFAULT_CENTRAL_TARGET,
185
+ ) -> RemovedSourceAuditResult:
186
+ root = root.resolve()
187
+ audit_paths = sorted(set(paths or _extract_removed_source_paths(root, central_target)))
188
+ findings = [
189
+ RemovedSourceFinding(
190
+ path=removed_path,
191
+ exists=(root / removed_path).exists(),
192
+ registered_refs=_manifest_refs(root, removed_path),
193
+ source_file_style_refs=_source_file_style_refs(root, central_target, removed_path),
194
+ live_refs=_live_refs(root, central_target, removed_path),
195
+ )
196
+ for removed_path in audit_paths
197
+ ]
198
+ status = "PASS" if all(finding.ok for finding in findings) else "FAIL"
199
+ return RemovedSourceAuditResult(
200
+ result_status=status,
201
+ central_target=central_target,
202
+ audited_count=len(findings),
203
+ findings=findings,
204
+ )
205
+
206
+
207
+ def render_removed_source_audit(result: RemovedSourceAuditResult) -> str:
208
+ lines = [
209
+ "REMOVED_SOURCE_AUDIT",
210
+ f"STATUS={result.result_status}",
211
+ f"CENTRAL_TARGET={result.central_target}",
212
+ f"AUDITED={result.audited_count}",
213
+ ]
214
+ for finding in result.findings:
215
+ lines.append(f"- PATH={finding.path} OK={finding.ok}")
216
+ if finding.exists:
217
+ lines.append(" EXISTS=yes")
218
+ for label, refs in (
219
+ ("REGISTERED_REF", finding.registered_refs),
220
+ ("SOURCE_FILES_STYLE_REF", finding.source_file_style_refs),
221
+ ("LIVE_REF", finding.live_refs),
222
+ ):
223
+ for ref in refs:
224
+ lines.append(f" {label} {ref}")
225
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,13 @@
1
+ from pathlib import Path
2
+ from string import Template
3
+
4
+
5
+ def render_template_string(template: str, context: dict[str, str]) -> str:
6
+ return Template(template).safe_substitute(context)
7
+
8
+
9
+ def write_file(path: Path, content: str, overwrite: bool = False) -> None:
10
+ if path.exists() and not overwrite:
11
+ raise FileExistsError(f"Refusing to overwrite existing file: {path}")
12
+ path.parent.mkdir(parents=True, exist_ok=True)
13
+ path.write_text(content.lstrip(), encoding="utf-8")