@ngocsangairvds/vsaf 3.1.27 → 3.2.1

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 (303) hide show
  1. package/package.json +2 -2
  2. package/src/global.js +70 -10
  3. package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
  4. package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
  5. package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
  6. package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
  7. package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
  8. package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
  9. package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
  10. package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
  11. package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
  12. package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
  13. package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
  14. package/tools/skills/vsaf-build/SKILL.md +32 -2
  15. package/tools/skills/vsaf-ship/SKILL.md +41 -10
  16. package/tools/skills/vsaf-test/SKILL.md +8 -0
  17. package/tools/vds-scripts/.mcp.json +11 -0
  18. package/tools/vds-scripts/.secrets.baseline +133 -0
  19. package/tools/vds-scripts/AGENTS.md +152 -0
  20. package/tools/vds-scripts/CLAUDE.md +101 -0
  21. package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
  22. package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
  23. package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
  24. package/tools/vds-scripts/README.md +97 -0
  25. package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
  26. package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
  27. package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
  28. package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
  29. package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
  30. package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
  31. package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
  32. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
  33. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
  34. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
  35. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
  36. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
  37. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
  38. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
  39. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
  40. package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
  41. package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
  42. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
  43. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
  44. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
  45. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
  46. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
  47. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
  48. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
  49. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
  50. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
  51. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
  52. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
  53. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
  54. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
  55. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
  56. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
  57. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
  58. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
  59. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
  60. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
  61. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
  62. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
  63. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
  64. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
  65. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
  66. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
  67. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
  68. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
  69. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
  70. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
  71. package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
  72. package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
  73. package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
  74. package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
  75. package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
  76. package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
  77. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
  78. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
  79. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
  80. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
  81. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
  82. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
  83. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
  84. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
  85. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
  86. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
  87. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
  88. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
  89. package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
  90. package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
  91. package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
  92. package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
  93. package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
  94. package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
  95. package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
  96. package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
  97. package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
  98. package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
  99. package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
  100. package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
  101. package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
  102. package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
  103. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
  104. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
  105. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
  106. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
  107. package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
  108. package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
  109. package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
  110. package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
  111. package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
  112. package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
  113. package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
  114. package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
  115. package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
  116. package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
  117. package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
  118. package/tools/vds-scripts/git_orchestrator/README.md +280 -0
  119. package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
  120. package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
  121. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
  122. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
  123. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
  124. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
  125. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
  126. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
  127. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
  128. package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
  129. package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
  130. package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
  131. package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
  132. package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
  133. package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
  134. package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
  135. package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
  136. package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
  137. package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
  138. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
  139. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
  140. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
  141. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
  142. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
  143. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
  144. package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
  145. package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
  146. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
  147. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
  148. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
  149. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
  150. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
  151. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
  152. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
  153. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
  154. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
  155. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
  156. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
  157. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
  158. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
  159. package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
  160. package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
  161. package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
  162. package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
  163. package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
  164. package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
  165. package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
  166. package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
  167. package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
  168. package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
  169. package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
  170. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
  171. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
  172. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
  173. package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
  174. package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
  175. package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
  176. package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
  177. package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
  178. package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
  179. package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
  180. package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
  181. package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
  182. package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
  183. package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
  184. package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
  185. package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
  186. package/tools/vds-scripts/mcp_server/README.md +140 -0
  187. package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
  188. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
  189. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
  190. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
  191. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
  192. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
  193. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
  194. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
  195. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
  196. package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
  197. package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
  198. package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
  199. package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
  200. package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
  201. package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
  202. package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
  203. package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
  204. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
  205. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
  206. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
  207. package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
  208. package/tools/vds-scripts/mypy.ini +5 -0
  209. package/tools/vds-scripts/pyproject.toml +29 -0
  210. package/tools/vds-scripts/repo-manifest.yaml +273 -0
  211. package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
  212. package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
  213. package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
  214. package/tools/vds-scripts/scripts/README.md +162 -0
  215. package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
  216. package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
  217. package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
  218. package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
  219. package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
  220. package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
  221. package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
  222. package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
  223. package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
  224. package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
  225. package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
  226. package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
  227. package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
  228. package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
  229. package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
  230. package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
  231. package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
  232. package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
  233. package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
  234. package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
  235. package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
  236. package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
  237. package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
  238. package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
  239. package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
  240. package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
  241. package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
  242. package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
  243. package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
  244. package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
  245. package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
  246. package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
  247. package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
  248. package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
  249. package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
  250. package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
  251. package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
  252. package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
  253. package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
  254. package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
  255. package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
  256. package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
  257. package/tools/vds-scripts/uv.lock +8 -0
  258. package/tools/vds-scripts/vds_cli/README.md +126 -0
  259. package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
  260. package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
  261. package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
  262. package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
  263. package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
  264. package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
  265. package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
  266. package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
  267. package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
  268. package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
  269. package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
  270. package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
  271. package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
  272. package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
  273. package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
  274. package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
  275. package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
  276. package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
  277. package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
  278. package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
  279. package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
  280. package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
  281. package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
  282. package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
  283. package/tools/vds-scripts/vds_cli/uv.lock +524 -0
  284. package/tools/vds-scripts/vds_cli_common/README.md +190 -0
  285. package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
  286. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
  287. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
  288. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
  289. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
  290. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
  291. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
  292. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
  293. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
  294. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
  295. package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
  296. package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
  297. package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
  298. package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
  299. package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
  300. package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
  301. package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
  302. package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
  303. package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
@@ -0,0 +1,1203 @@
1
+ """AGENTS-first sync engine for AGENTS plus legacy mirrored instruction files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ import time
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass, field
12
+ from datetime import UTC, datetime
13
+ from difflib import SequenceMatcher
14
+ from errno import EACCES, EBUSY, EDEADLK, EPERM
15
+ from pathlib import Path
16
+ from typing import Protocol, TypeVar
17
+
18
+ from .sync_api import (
19
+ ConfluencePropertySyncApiClient,
20
+ SyncApiClient,
21
+ SyncApiDocument,
22
+ SyncApiError,
23
+ SyncApiRevisionConflict,
24
+ SyncApiTransientError,
25
+ VdsAiMemorySyncApiClient,
26
+ )
27
+
28
+ WatchBackend = str
29
+ _T = TypeVar("_T")
30
+
31
+ TARGET_FILES: tuple[str, str, str] = (
32
+ "AGENTS.md",
33
+ "CLAUDE.md",
34
+ ".github/copilot-instructions.md",
35
+ )
36
+
37
+
38
+ class SyncError(RuntimeError):
39
+ """Base error for instruction sync failures."""
40
+
41
+
42
+ class RevisionConflictError(SyncError):
43
+ """Raised when central state revision does not match expected revision."""
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class CentralDocument:
48
+ """Canonical central-state document."""
49
+
50
+ content: str
51
+ revision: str
52
+ content_hash: str
53
+ source_file: str
54
+ updated_at: str
55
+
56
+
57
+ class CentralStateBackend(Protocol):
58
+ """Central-state adapter interface."""
59
+
60
+ def pull(self) -> CentralDocument | None:
61
+ """Return current canonical document or None when not initialized."""
62
+
63
+ def push_if_match(
64
+ self,
65
+ *,
66
+ content: str,
67
+ source_file: str,
68
+ expected_revision: str | None,
69
+ ) -> CentralDocument:
70
+ """Update canonical document using optimistic concurrency semantics."""
71
+
72
+
73
+ @dataclass
74
+ class LocalJsonCentralStateBackend:
75
+ """Local JSON-backed central state with If-Match style revision checks."""
76
+
77
+ state_path: Path
78
+
79
+ def pull(self) -> CentralDocument | None:
80
+ if not self.state_path.exists():
81
+ return None
82
+ payload = json.loads(self.state_path.read_text(encoding="utf-8"))
83
+ return CentralDocument(
84
+ content=payload["content"],
85
+ revision=str(payload["revision"]),
86
+ content_hash=payload["content_hash"],
87
+ source_file=payload["source_file"],
88
+ updated_at=payload["updated_at"],
89
+ )
90
+
91
+ def push_if_match(
92
+ self,
93
+ *,
94
+ content: str,
95
+ source_file: str,
96
+ expected_revision: str | None,
97
+ ) -> CentralDocument:
98
+ current = self.pull()
99
+ if expected_revision is None:
100
+ if current is not None:
101
+ raise RevisionConflictError(
102
+ f"Expected empty central state but found revision {current.revision}."
103
+ )
104
+ elif current is None or current.revision != expected_revision:
105
+ actual = current.revision if current else "<missing>"
106
+ raise RevisionConflictError(
107
+ f"Central revision mismatch (expected={expected_revision}, actual={actual})."
108
+ )
109
+
110
+ revision = "1" if current is None else str(int(current.revision) + 1)
111
+ now = datetime.now(UTC).isoformat()
112
+ payload = {
113
+ "content": content,
114
+ "revision": revision,
115
+ "content_hash": _sha256(content),
116
+ "source_file": source_file,
117
+ "updated_at": now,
118
+ }
119
+ _write_json_atomic(self.state_path, payload)
120
+ return CentralDocument(
121
+ content=payload["content"],
122
+ revision=payload["revision"],
123
+ content_hash=payload["content_hash"],
124
+ source_file=payload["source_file"],
125
+ updated_at=payload["updated_at"],
126
+ )
127
+
128
+
129
+ @dataclass
130
+ class HttpCentralStateBackend:
131
+ """HTTP-backed central state with optimistic concurrency semantics."""
132
+
133
+ client: SyncApiClient
134
+ retry_attempts: int = 3
135
+ retry_base_delay_seconds: float = 0.2
136
+ retry_max_delay_seconds: float = 1.0
137
+
138
+ def pull(self) -> CentralDocument | None:
139
+ try:
140
+ document = self._retry_transient_errors("pull", self.client.pull_document)
141
+ except SyncApiError as exc:
142
+ raise SyncError(f"Failed pulling central state via HTTP: {exc}") from exc
143
+ if document is None:
144
+ return None
145
+ return _as_central_document(document)
146
+
147
+ def push_if_match(
148
+ self,
149
+ *,
150
+ content: str,
151
+ source_file: str,
152
+ expected_revision: str | None,
153
+ ) -> CentralDocument:
154
+ try:
155
+ document = self._retry_transient_errors(
156
+ "push",
157
+ lambda: self.client.push_document_if_match(
158
+ content=content,
159
+ source_file=source_file,
160
+ expected_revision=expected_revision,
161
+ ),
162
+ )
163
+ except SyncApiRevisionConflict as exc:
164
+ raise RevisionConflictError(str(exc)) from exc
165
+ except SyncApiError as exc:
166
+ raise SyncError(f"Failed pushing central state via HTTP: {exc}") from exc
167
+ return _as_central_document(document)
168
+
169
+ def _retry_transient_errors(self, operation: str, request_fn: Callable[[], _T]) -> _T:
170
+ attempts = max(self.retry_attempts, 1)
171
+ for attempt in range(1, attempts + 1):
172
+ try:
173
+ return request_fn()
174
+ except SyncApiTransientError:
175
+ if attempt >= attempts:
176
+ raise
177
+ delay = min(
178
+ self.retry_base_delay_seconds * (2 ** (attempt - 1)),
179
+ self.retry_max_delay_seconds,
180
+ )
181
+ time.sleep(delay)
182
+ except SyncApiRevisionConflict:
183
+ raise
184
+ raise SyncError(f"Unexpected retry state for operation '{operation}'.")
185
+
186
+
187
+ @dataclass
188
+ class SyncLedger:
189
+ """Local sync ledger for incremental conflict detection."""
190
+
191
+ last_revision: str | None = None
192
+ last_source_file: str | None = None
193
+ file_hashes: dict[str, str] = field(default_factory=dict)
194
+ synced_hash: str | None = None
195
+ synced_content: str | None = None
196
+
197
+ @classmethod
198
+ def load(cls, path: Path) -> SyncLedger:
199
+ if not path.exists():
200
+ return cls()
201
+ payload = json.loads(path.read_text(encoding="utf-8"))
202
+ return cls(
203
+ last_revision=payload.get("last_revision"),
204
+ last_source_file=payload.get("last_source_file"),
205
+ file_hashes=dict(payload.get("file_hashes", {})),
206
+ synced_hash=payload.get("synced_hash"),
207
+ synced_content=payload.get("synced_content"),
208
+ )
209
+
210
+ def save(self, path: Path) -> None:
211
+ payload = {
212
+ "last_revision": self.last_revision,
213
+ "last_source_file": self.last_source_file,
214
+ "file_hashes": self.file_hashes,
215
+ "synced_hash": self.synced_hash,
216
+ "synced_content": self.synced_content,
217
+ }
218
+ _write_json_atomic(path, payload)
219
+
220
+
221
+ @dataclass(frozen=True)
222
+ class SyncConfig:
223
+ """Runtime configuration for watch-agents sync."""
224
+
225
+ repo_root: Path
226
+ target_files: tuple[str, str, str] = TARGET_FILES
227
+ workspace_dirname: str = ".vds-sync"
228
+ reconcile_interval_seconds: int = 300
229
+ debounce_ms: int = 500
230
+ watch_backend: WatchBackend = "auto"
231
+ central_backend: str = "local"
232
+ central_contract: str = "generic"
233
+ central_state_url: str | None = None
234
+ central_timeout_seconds: int = 10
235
+ central_auth_token: str | None = None
236
+ central_memory_id: str | None = None
237
+ central_user_id: str = "vds-sync-agent"
238
+ central_session_id: str = "vds-sync-session"
239
+ central_confluence_server: str = "internal"
240
+ central_confluence_page_id: str | None = None
241
+ central_confluence_property_key: str = "vds_sync_instructions"
242
+ dry_run: bool = False
243
+ log_json: bool = True
244
+ lock_stale_seconds: int = 900
245
+ lock_timeout_seconds: int = 5
246
+ conflict_dir: Path | None = None
247
+
248
+ @property
249
+ def sync_dir(self) -> Path:
250
+ return self.repo_root / self.workspace_dirname
251
+
252
+ @property
253
+ def central_state_path(self) -> Path:
254
+ return self.sync_dir / "instructions-central-state.json"
255
+
256
+ @property
257
+ def ledger_path(self) -> Path:
258
+ return self.sync_dir / "watch-agents-ledger.json"
259
+
260
+ @property
261
+ def lock_path(self) -> Path:
262
+ return self.sync_dir / "watch-agents.lock"
263
+
264
+ @property
265
+ def log_path(self) -> Path:
266
+ return self.sync_dir / "watch-agents.log.jsonl"
267
+
268
+ @property
269
+ def resolved_conflict_dir(self) -> Path:
270
+ return self.conflict_dir if self.conflict_dir is not None else self.sync_dir / "conflicts"
271
+
272
+
273
+ @dataclass(frozen=True)
274
+ class FileSnapshot:
275
+ """Snapshot of one target file."""
276
+
277
+ relpath: str
278
+ abspath: Path
279
+ content: str
280
+ content_hash: str
281
+ mtime: float
282
+
283
+
284
+ @dataclass(frozen=True)
285
+ class LineEdit:
286
+ """A line-range replacement edit for conservative three-way merge."""
287
+
288
+ start: int
289
+ end: int
290
+ replacement: tuple[str, ...]
291
+
292
+
293
+ class SyncFileLock:
294
+ """Simple cross-platform lock-file with stale lock eviction."""
295
+
296
+ def __init__(self, path: Path, *, stale_seconds: int, timeout_seconds: int) -> None:
297
+ self.path = path
298
+ self.stale_seconds = stale_seconds
299
+ self.timeout_seconds = timeout_seconds
300
+
301
+ def __enter__(self) -> SyncFileLock:
302
+ deadline = time.time() + self.timeout_seconds
303
+ self.path.parent.mkdir(parents=True, exist_ok=True)
304
+
305
+ while True:
306
+ try:
307
+ fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
308
+ try:
309
+ payload = {
310
+ "pid": os.getpid(),
311
+ "created_at": time.time(),
312
+ }
313
+ os.write(fd, json.dumps(payload).encode("utf-8"))
314
+ finally:
315
+ os.close(fd)
316
+ return self
317
+ except FileExistsError:
318
+ if self._is_stale():
319
+ self.path.unlink(missing_ok=True)
320
+ continue
321
+ if time.time() > deadline:
322
+ raise SyncError(f"Could not acquire lock: {self.path}") from None
323
+ time.sleep(0.1)
324
+
325
+ def __exit__(self, *_args: object) -> None:
326
+ self.path.unlink(missing_ok=True)
327
+
328
+ def _is_stale(self) -> bool:
329
+ try:
330
+ payload = json.loads(self.path.read_text(encoding="utf-8"))
331
+ created_at = float(payload.get("created_at", 0))
332
+ except (json.JSONDecodeError, OSError, ValueError):
333
+ return True
334
+ return (time.time() - created_at) > self.stale_seconds
335
+
336
+
337
+ class DocsSyncEngine:
338
+ """Core multi-writer reconciliation engine."""
339
+
340
+ def __init__(self, *, config: SyncConfig, backend: CentralStateBackend | None = None) -> None:
341
+ self.config = config
342
+ self.backend = backend or _build_backend(config)
343
+ self.ledger = SyncLedger.load(config.ledger_path)
344
+
345
+ def run_once(self, *, reason: str) -> str:
346
+ snapshots = self._collect_snapshots()
347
+ central = self.backend.pull()
348
+
349
+ if self._needs_startup_strategy(central):
350
+ return self._startup_reconcile(snapshots=snapshots, central=central, reason=reason)
351
+
352
+ return self._steady_state_reconcile(snapshots=snapshots, central=central, reason=reason)
353
+
354
+ def watch(self, *, once: bool = False) -> None:
355
+ with SyncFileLock(
356
+ self.config.lock_path,
357
+ stale_seconds=self.config.lock_stale_seconds,
358
+ timeout_seconds=self.config.lock_timeout_seconds,
359
+ ):
360
+ self.run_once(reason="startup")
361
+ if once:
362
+ return
363
+
364
+ watch_backend = self._resolve_watch_backend()
365
+ self._log("watch-backend-selected", backend=watch_backend)
366
+ if watch_backend == "watchfiles":
367
+ self._watch_loop_watchfiles()
368
+ return
369
+ self._watch_loop_polling()
370
+
371
+ def _watch_loop_polling(self) -> None:
372
+ last_periodic = time.monotonic()
373
+ previous_hashes = self._snapshot_hash_map(self._collect_snapshots())
374
+
375
+ while True:
376
+ time.sleep(max(self.config.debounce_ms / 1000.0, 0.2))
377
+
378
+ current = self._collect_snapshots()
379
+ current_hashes = self._snapshot_hash_map(current)
380
+ if current_hashes != previous_hashes:
381
+ self.run_once(reason="event")
382
+ previous_hashes = self._snapshot_hash_map(self._collect_snapshots())
383
+
384
+ now = time.monotonic()
385
+ if now - last_periodic >= self.config.reconcile_interval_seconds:
386
+ self.run_once(reason="periodic")
387
+ previous_hashes = self._snapshot_hash_map(self._collect_snapshots())
388
+ last_periodic = now
389
+
390
+ def _watch_loop_watchfiles(self) -> None:
391
+ watch = _load_watchfiles_watch()
392
+ if watch is None:
393
+ raise SyncError(
394
+ "watch_backend='watchfiles' requested but 'watchfiles' package is not installed."
395
+ )
396
+
397
+ last_periodic = time.monotonic()
398
+ target_files = {(self.config.repo_root / relpath).resolve() for relpath in self.config.target_files}
399
+ rust_timeout_ms = max(200, min(self.config.debounce_ms, 2000))
400
+
401
+ for changes in watch(
402
+ self.config.repo_root,
403
+ recursive=True,
404
+ debounce=self.config.debounce_ms,
405
+ yield_on_timeout=True,
406
+ rust_timeout=rust_timeout_ms,
407
+ ):
408
+ has_target_change = False
409
+ for _change, changed_path in changes:
410
+ try:
411
+ resolved = Path(changed_path).resolve()
412
+ except OSError:
413
+ continue
414
+ if resolved in target_files:
415
+ has_target_change = True
416
+ break
417
+
418
+ if has_target_change:
419
+ self.run_once(reason="event")
420
+
421
+ now = time.monotonic()
422
+ if now - last_periodic >= self.config.reconcile_interval_seconds:
423
+ self.run_once(reason="periodic")
424
+ last_periodic = now
425
+
426
+ def _resolve_watch_backend(self) -> WatchBackend:
427
+ configured = self.config.watch_backend.lower().strip()
428
+ if configured not in {"auto", "watchfiles", "polling"}:
429
+ raise SyncError(
430
+ f"Invalid watch backend '{self.config.watch_backend}'. Expected: auto, watchfiles, polling."
431
+ )
432
+ if configured == "polling":
433
+ return "polling"
434
+ if configured == "watchfiles":
435
+ if _load_watchfiles_watch() is None:
436
+ raise SyncError(
437
+ "watch_backend='watchfiles' requested but 'watchfiles' package is not installed."
438
+ )
439
+ return "watchfiles"
440
+ if _load_watchfiles_watch() is not None:
441
+ return "watchfiles"
442
+ return "polling"
443
+
444
+ def _startup_reconcile(
445
+ self,
446
+ *,
447
+ snapshots: dict[str, FileSnapshot],
448
+ central: CentralDocument | None,
449
+ reason: str,
450
+ ) -> str:
451
+ local_hashes = {rel: snap.content_hash for rel, snap in snapshots.items()}
452
+ unique_hashes = set(local_hashes.values())
453
+
454
+ if central is None:
455
+ source = self._select_local_source_without_baseline(snapshots)
456
+ if source is None:
457
+ artifact = self._write_conflict_artifact(
458
+ reason="startup-local-conflict",
459
+ snapshots=snapshots,
460
+ central=central,
461
+ )
462
+ self._log(
463
+ "conflict",
464
+ reason=reason,
465
+ artifact=str(artifact),
466
+ conflict_state="artifact-written",
467
+ write_outcomes={},
468
+ )
469
+ return "conflict"
470
+ canonical = self.backend.push_if_match(
471
+ content=snapshots[source].content,
472
+ source_file=source,
473
+ expected_revision=None,
474
+ )
475
+ write_outcomes = self._fan_out(
476
+ canonical.content,
477
+ canonical_hash=canonical.content_hash,
478
+ )
479
+ self._update_ledger(canonical=canonical)
480
+ self._log(
481
+ "sync",
482
+ reason=reason,
483
+ source_file=source,
484
+ source_hash=snapshots[source].content_hash,
485
+ revision=canonical.revision,
486
+ conflict_state="none",
487
+ write_outcomes=write_outcomes,
488
+ )
489
+ return "synced"
490
+
491
+ if len(unique_hashes) == 1:
492
+ only_hash = next(iter(unique_hashes))
493
+ if only_hash == central.content_hash:
494
+ self._update_ledger(canonical=central)
495
+ self._log(
496
+ "noop",
497
+ reason=reason,
498
+ source_file=central.source_file,
499
+ source_hash=central.content_hash,
500
+ revision=central.revision,
501
+ conflict_state="none",
502
+ write_outcomes={},
503
+ )
504
+ return "noop"
505
+ write_outcomes = self._fan_out(
506
+ central.content,
507
+ canonical_hash=central.content_hash,
508
+ )
509
+ self._update_ledger(canonical=central)
510
+ self._log(
511
+ "fanout",
512
+ reason=reason,
513
+ source_file=central.source_file,
514
+ source_hash=central.content_hash,
515
+ revision=central.revision,
516
+ conflict_state="none",
517
+ write_outcomes=write_outcomes,
518
+ )
519
+ return "fanout"
520
+
521
+ source = self._select_single_modified_candidate(snapshots, baseline_hash=central.content_hash)
522
+ if source is None:
523
+ artifact = self._write_conflict_artifact(
524
+ reason="startup-divergent-local-state",
525
+ snapshots=snapshots,
526
+ central=central,
527
+ )
528
+ self._log(
529
+ "conflict",
530
+ reason=reason,
531
+ artifact=str(artifact),
532
+ revision=central.revision,
533
+ conflict_state="artifact-written",
534
+ write_outcomes={},
535
+ )
536
+ return "conflict"
537
+
538
+ try:
539
+ canonical = self.backend.push_if_match(
540
+ content=snapshots[source].content,
541
+ source_file=source,
542
+ expected_revision=central.revision,
543
+ )
544
+ except RevisionConflictError:
545
+ retried = self._retry_with_safe_merge(
546
+ source=source,
547
+ local_content=snapshots[source].content,
548
+ reason=f"{reason}:startup-central-revision-conflict",
549
+ )
550
+ if retried is not None:
551
+ write_outcomes = self._fan_out(
552
+ retried.content,
553
+ canonical_hash=retried.content_hash,
554
+ )
555
+ self._update_ledger(canonical=retried)
556
+ self._log(
557
+ "sync-merge-retry",
558
+ reason=reason,
559
+ source_file=source,
560
+ source_hash=snapshots[source].content_hash,
561
+ revision=retried.revision,
562
+ conflict_state="merge-applied",
563
+ write_outcomes=write_outcomes,
564
+ )
565
+ return "synced"
566
+ artifact = self._write_conflict_artifact(
567
+ reason="startup-central-revision-conflict",
568
+ snapshots=snapshots,
569
+ central=self.backend.pull(),
570
+ )
571
+ self._log(
572
+ "conflict",
573
+ reason=reason,
574
+ artifact=str(artifact),
575
+ source_file=source,
576
+ source_hash=snapshots[source].content_hash,
577
+ conflict_state="artifact-written",
578
+ write_outcomes={},
579
+ )
580
+ return "conflict"
581
+
582
+ write_outcomes = self._fan_out(
583
+ canonical.content,
584
+ canonical_hash=canonical.content_hash,
585
+ )
586
+ self._update_ledger(canonical=canonical)
587
+ self._log(
588
+ "sync",
589
+ reason=reason,
590
+ source_file=source,
591
+ source_hash=snapshots[source].content_hash,
592
+ revision=canonical.revision,
593
+ conflict_state="none",
594
+ write_outcomes=write_outcomes,
595
+ )
596
+ return "synced"
597
+
598
+ def _steady_state_reconcile(
599
+ self,
600
+ *,
601
+ snapshots: dict[str, FileSnapshot],
602
+ central: CentralDocument | None,
603
+ reason: str,
604
+ ) -> str:
605
+ changed = [
606
+ rel
607
+ for rel, snap in snapshots.items()
608
+ if self.ledger.file_hashes.get(rel) != snap.content_hash
609
+ ]
610
+
611
+ if not changed:
612
+ if central and any(s.content_hash != central.content_hash for s in snapshots.values()):
613
+ write_outcomes = self._fan_out(
614
+ central.content,
615
+ canonical_hash=central.content_hash,
616
+ )
617
+ self._update_ledger(canonical=central)
618
+ self._log(
619
+ "fanout",
620
+ reason=reason,
621
+ source_file=central.source_file,
622
+ source_hash=central.content_hash,
623
+ revision=central.revision,
624
+ conflict_state="none",
625
+ write_outcomes=write_outcomes,
626
+ )
627
+ return "fanout"
628
+ self._log("noop", reason=reason, conflict_state="none", write_outcomes={})
629
+ return "noop"
630
+
631
+ distinct_hashes = {snapshots[rel].content_hash for rel in changed}
632
+ if len(changed) > 1 and len(distinct_hashes) > 1:
633
+ artifact = self._write_conflict_artifact(
634
+ reason="divergent-local-edits",
635
+ snapshots=snapshots,
636
+ central=central,
637
+ )
638
+ self._log(
639
+ "conflict",
640
+ reason=reason,
641
+ artifact=str(artifact),
642
+ conflict_state="artifact-written",
643
+ write_outcomes={},
644
+ )
645
+ return "conflict"
646
+
647
+ source = changed[0]
648
+ expected_revision = self.ledger.last_revision or (central.revision if central else None)
649
+ if expected_revision is None:
650
+ expected_revision = None
651
+
652
+ try:
653
+ canonical = self.backend.push_if_match(
654
+ content=snapshots[source].content,
655
+ source_file=source,
656
+ expected_revision=expected_revision,
657
+ )
658
+ except RevisionConflictError:
659
+ retried = self._retry_with_safe_merge(
660
+ source=source,
661
+ local_content=snapshots[source].content,
662
+ reason=f"{reason}:central-revision-mismatch",
663
+ )
664
+ if retried is not None:
665
+ write_outcomes = self._fan_out(
666
+ retried.content,
667
+ canonical_hash=retried.content_hash,
668
+ )
669
+ self._update_ledger(canonical=retried)
670
+ self._log(
671
+ "sync-merge-retry",
672
+ reason=reason,
673
+ source_file=source,
674
+ source_hash=snapshots[source].content_hash,
675
+ revision=retried.revision,
676
+ conflict_state="merge-applied",
677
+ write_outcomes=write_outcomes,
678
+ )
679
+ return "synced"
680
+ artifact = self._write_conflict_artifact(
681
+ reason="central-revision-mismatch",
682
+ snapshots=snapshots,
683
+ central=self.backend.pull(),
684
+ )
685
+ self._log(
686
+ "conflict",
687
+ reason=reason,
688
+ artifact=str(artifact),
689
+ source_file=source,
690
+ source_hash=snapshots[source].content_hash,
691
+ conflict_state="artifact-written",
692
+ write_outcomes={},
693
+ )
694
+ return "conflict"
695
+
696
+ write_outcomes = self._fan_out(
697
+ canonical.content,
698
+ canonical_hash=canonical.content_hash,
699
+ )
700
+ self._update_ledger(canonical=canonical)
701
+ self._log(
702
+ "sync",
703
+ reason=reason,
704
+ source_file=source,
705
+ source_hash=snapshots[source].content_hash,
706
+ revision=canonical.revision,
707
+ conflict_state="none",
708
+ write_outcomes=write_outcomes,
709
+ )
710
+ return "synced"
711
+
712
+ def _collect_snapshots(self) -> dict[str, FileSnapshot]:
713
+ snapshots: dict[str, FileSnapshot] = {}
714
+ for relpath in self.config.target_files:
715
+ abspath = self.config.repo_root / relpath
716
+ if not abspath.exists():
717
+ raise SyncError(f"Missing required file for sync: {abspath}")
718
+ content = abspath.read_text(encoding="utf-8")
719
+ stat = abspath.stat()
720
+ snapshots[relpath] = FileSnapshot(
721
+ relpath=relpath,
722
+ abspath=abspath,
723
+ content=content,
724
+ content_hash=_sha256(content),
725
+ mtime=stat.st_mtime,
726
+ )
727
+ return snapshots
728
+
729
+ def _fan_out(self, content: str, *, canonical_hash: str) -> dict[str, str]:
730
+ outcomes: dict[str, str] = {}
731
+ for relpath in self.config.target_files:
732
+ path = self.config.repo_root / relpath
733
+ current = path.read_text(encoding="utf-8")
734
+ if _sha256(current) == canonical_hash:
735
+ outcomes[relpath] = "unchanged"
736
+ continue
737
+ if self.config.dry_run:
738
+ outcomes[relpath] = "dry-run-skip"
739
+ continue
740
+ _atomic_write_text(path, content)
741
+ outcomes[relpath] = "updated"
742
+ return outcomes
743
+
744
+ def _update_ledger(self, *, canonical: CentralDocument) -> None:
745
+ snapshots = self._collect_snapshots()
746
+ self.ledger.last_revision = canonical.revision
747
+ self.ledger.last_source_file = canonical.source_file
748
+ self.ledger.synced_hash = canonical.content_hash
749
+ self.ledger.synced_content = canonical.content
750
+ self.ledger.file_hashes = {rel: snap.content_hash for rel, snap in snapshots.items()}
751
+ if not self.config.dry_run:
752
+ self.config.sync_dir.mkdir(parents=True, exist_ok=True)
753
+ self.ledger.save(self.config.ledger_path)
754
+
755
+ def _write_conflict_artifact(
756
+ self,
757
+ *,
758
+ reason: str,
759
+ snapshots: dict[str, FileSnapshot],
760
+ central: CentralDocument | None,
761
+ ) -> Path:
762
+ conflict_dir = self.config.resolved_conflict_dir
763
+ conflict_dir.mkdir(parents=True, exist_ok=True)
764
+ ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
765
+ artifact = conflict_dir / f".sync-conflict-{ts}.md"
766
+
767
+ lines = [
768
+ "# Sync Conflict",
769
+ "",
770
+ f"- reason: {reason}",
771
+ f"- timestamp_utc: {datetime.now(UTC).isoformat()}",
772
+ f"- central_revision: {central.revision if central else '<none>'}",
773
+ f"- central_hash: {central.content_hash if central else '<none>'}",
774
+ "",
775
+ "## Local Hashes",
776
+ ]
777
+ for relpath in self.config.target_files:
778
+ lines.append(f"- {relpath}: {snapshots[relpath].content_hash}")
779
+
780
+ lines.extend(
781
+ [
782
+ "",
783
+ "## Next Actions",
784
+ "- Resolve conflicting edits manually.",
785
+ "- Re-run `vds-cli docs watch-agents --once --repo-root <repo>` after resolution.",
786
+ ]
787
+ )
788
+
789
+ _atomic_write_text(artifact, "\n".join(lines) + "\n")
790
+ return artifact
791
+
792
+ def _needs_startup_strategy(self, central: CentralDocument | None) -> bool:
793
+ if not self.ledger.file_hashes:
794
+ return True
795
+ if central and self.ledger.last_revision is None:
796
+ return True
797
+ return False
798
+
799
+ def _snapshot_hash_map(self, snapshots: dict[str, FileSnapshot]) -> dict[str, str]:
800
+ return {rel: snap.content_hash for rel, snap in snapshots.items()}
801
+
802
+ def _select_local_source_without_baseline(self, snapshots: dict[str, FileSnapshot]) -> str | None:
803
+ hashes = {rel: snap.content_hash for rel, snap in snapshots.items()}
804
+ groups = _hash_groups(hashes)
805
+ if len(groups) == 1:
806
+ return self.config.target_files[0]
807
+ if len(groups) == 2:
808
+ newest = max(snapshots.values(), key=lambda item: item.mtime)
809
+ return newest.relpath
810
+ return None
811
+
812
+ def _select_single_modified_candidate(
813
+ self,
814
+ snapshots: dict[str, FileSnapshot],
815
+ *,
816
+ baseline_hash: str,
817
+ ) -> str | None:
818
+ non_baseline = [rel for rel, snap in snapshots.items() if snap.content_hash != baseline_hash]
819
+ if len(non_baseline) == 1:
820
+ return non_baseline[0]
821
+ return None
822
+
823
+ def _retry_with_safe_merge(
824
+ self,
825
+ *,
826
+ source: str,
827
+ local_content: str,
828
+ reason: str,
829
+ ) -> CentralDocument | None:
830
+ latest = self.backend.pull()
831
+ if latest is None:
832
+ return None
833
+ merged = self._safe_three_way_merge(
834
+ base_content=self.ledger.synced_content,
835
+ local_content=local_content,
836
+ remote_content=latest.content,
837
+ )
838
+ if merged is None:
839
+ return None
840
+ try:
841
+ canonical = self.backend.push_if_match(
842
+ content=merged,
843
+ source_file=source,
844
+ expected_revision=latest.revision,
845
+ )
846
+ except RevisionConflictError:
847
+ return None
848
+ self._log(
849
+ "merge-retry-applied",
850
+ reason=reason,
851
+ source_file=source,
852
+ source_hash=_sha256(local_content),
853
+ base_revision=self.ledger.last_revision,
854
+ remote_revision=latest.revision,
855
+ merged_revision=canonical.revision,
856
+ conflict_state="merge-applied",
857
+ )
858
+ return canonical
859
+
860
+ def _safe_three_way_merge(
861
+ self,
862
+ *,
863
+ base_content: str | None,
864
+ local_content: str,
865
+ remote_content: str,
866
+ ) -> str | None:
867
+ if base_content is None:
868
+ return None
869
+ if local_content == remote_content:
870
+ return local_content
871
+ if local_content == base_content:
872
+ return remote_content
873
+ if remote_content == base_content:
874
+ return local_content
875
+
876
+ base_lines = base_content.splitlines(keepends=True)
877
+ local_edits = _line_edits(base_lines, local_content.splitlines(keepends=True))
878
+ remote_edits = _line_edits(base_lines, remote_content.splitlines(keepends=True))
879
+
880
+ merged_edits: list[LineEdit] = []
881
+ for edit in [*local_edits, *remote_edits]:
882
+ if any(
883
+ existing.start == edit.start
884
+ and existing.end == edit.end
885
+ and existing.replacement == edit.replacement
886
+ for existing in merged_edits
887
+ ):
888
+ continue
889
+ merged_edits.append(edit)
890
+
891
+ for index, left in enumerate(merged_edits):
892
+ for right in merged_edits[index + 1 :]:
893
+ if _edits_conflict(left, right):
894
+ return None
895
+
896
+ merged = list(base_lines)
897
+ for edit in sorted(merged_edits, key=lambda item: (item.start, item.end), reverse=True):
898
+ merged[edit.start : edit.end] = list(edit.replacement)
899
+ return "".join(merged)
900
+
901
+ def _log(self, event: str, **fields: object) -> None:
902
+ payload = {
903
+ "timestamp": datetime.now(UTC).isoformat(),
904
+ "event": event,
905
+ **fields,
906
+ }
907
+ line = json.dumps(payload, ensure_ascii=True)
908
+ if self.config.log_json:
909
+ print(line)
910
+ self.config.sync_dir.mkdir(parents=True, exist_ok=True)
911
+ for attempt in range(4):
912
+ try:
913
+ with self.config.log_path.open("a", encoding="utf-8") as fh:
914
+ fh.write(line + "\n")
915
+ return
916
+ except OSError as exc:
917
+ if exc.errno in {EDEADLK, EBUSY, EACCES, EPERM} and attempt < 3:
918
+ time.sleep(0.05 * (attempt + 1))
919
+ continue
920
+ # Do not crash sync loop because of local log sink write failures.
921
+ print(
922
+ json.dumps(
923
+ {
924
+ "timestamp": datetime.now(UTC).isoformat(),
925
+ "event": "log-write-failed",
926
+ "error": str(exc),
927
+ "log_path": str(self.config.log_path),
928
+ },
929
+ ensure_ascii=True,
930
+ ),
931
+ file=sys.stderr,
932
+ )
933
+ return
934
+
935
+
936
+ def find_repo_root(start: Path) -> Path:
937
+ """Find repository root by walking up until AGENTS and mirrored instruction files exist."""
938
+ current = start.resolve()
939
+ while True:
940
+ if all((current / relpath).exists() for relpath in TARGET_FILES):
941
+ return current
942
+ if current.parent == current:
943
+ raise SyncError(
944
+ "Could not locate repository root containing AGENTS.md plus mirrored legacy instruction files "
945
+ "(CLAUDE.md and .github/copilot-instructions.md)"
946
+ )
947
+ current = current.parent
948
+
949
+
950
+ def run_watch_agents(
951
+ *,
952
+ repo_root: Path,
953
+ reconcile_interval_seconds: int,
954
+ debounce_ms: int,
955
+ watch_backend: WatchBackend,
956
+ central_backend: str,
957
+ central_contract: str,
958
+ central_state_url: str | None,
959
+ central_timeout_seconds: int,
960
+ central_auth_token: str | None,
961
+ central_memory_id: str | None,
962
+ central_user_id: str,
963
+ central_session_id: str,
964
+ central_confluence_server: str,
965
+ central_confluence_page_id: str | None,
966
+ central_confluence_property_key: str,
967
+ dry_run: bool,
968
+ conflict_dir: Path | None,
969
+ log_json: bool,
970
+ once: bool,
971
+ ) -> None:
972
+ """Run the watch-agents command with resolved config."""
973
+ resolved_root = find_repo_root(repo_root)
974
+ config = SyncConfig(
975
+ repo_root=resolved_root,
976
+ reconcile_interval_seconds=reconcile_interval_seconds,
977
+ debounce_ms=debounce_ms,
978
+ watch_backend=watch_backend,
979
+ central_backend=central_backend,
980
+ central_contract=central_contract,
981
+ central_state_url=central_state_url,
982
+ central_timeout_seconds=central_timeout_seconds,
983
+ central_auth_token=central_auth_token,
984
+ central_memory_id=central_memory_id,
985
+ central_user_id=central_user_id,
986
+ central_session_id=central_session_id,
987
+ central_confluence_server=central_confluence_server,
988
+ central_confluence_page_id=central_confluence_page_id,
989
+ central_confluence_property_key=central_confluence_property_key,
990
+ dry_run=dry_run,
991
+ conflict_dir=conflict_dir,
992
+ log_json=log_json,
993
+ )
994
+ engine = DocsSyncEngine(config=config)
995
+ engine.watch(once=once)
996
+
997
+
998
+ def _build_backend(config: SyncConfig) -> CentralStateBackend:
999
+ backend = config.central_backend.lower().strip()
1000
+ if backend == "local":
1001
+ return LocalJsonCentralStateBackend(config.central_state_path)
1002
+ if backend == "http":
1003
+ contract = config.central_contract.lower().strip()
1004
+ if contract == "generic":
1005
+ if not config.central_state_url:
1006
+ raise SyncError(
1007
+ "central_contract='generic' requires --central-state-url."
1008
+ )
1009
+ return HttpCentralStateBackend(
1010
+ client=SyncApiClient(
1011
+ endpoint=config.central_state_url,
1012
+ timeout_seconds=config.central_timeout_seconds,
1013
+ auth_token=config.central_auth_token,
1014
+ )
1015
+ )
1016
+ if contract == "vds-ai-memory":
1017
+ base_url = config.central_state_url or os.getenv("VDS_AI_MEMORY_BASE_URL")
1018
+ if not base_url:
1019
+ raise SyncError(
1020
+ "central_contract='vds-ai-memory' requires --central-state-url or VDS_AI_MEMORY_BASE_URL."
1021
+ )
1022
+ memory_id = config.central_memory_id
1023
+ if not memory_id:
1024
+ raise SyncError(
1025
+ "central_contract='vds-ai-memory' requires --central-memory-id."
1026
+ )
1027
+ auth_token = config.central_auth_token or os.getenv("VDS_AI_MEMORY_API_KEY")
1028
+ return HttpCentralStateBackend(
1029
+ client=VdsAiMemorySyncApiClient(
1030
+ base_url=base_url,
1031
+ memory_id=memory_id,
1032
+ user_id=config.central_user_id,
1033
+ session_id=config.central_session_id,
1034
+ timeout_seconds=config.central_timeout_seconds,
1035
+ auth_token=auth_token,
1036
+ )
1037
+ )
1038
+ if contract == "confluence-property":
1039
+ page_id = config.central_confluence_page_id
1040
+ if not page_id:
1041
+ raise SyncError(
1042
+ "central_contract='confluence-property' requires --central-confluence-page-id."
1043
+ )
1044
+ base_url = _resolve_confluence_base_url(
1045
+ server=config.central_confluence_server,
1046
+ override=config.central_state_url,
1047
+ )
1048
+ auth_token = _resolve_confluence_token(
1049
+ server=config.central_confluence_server,
1050
+ explicit_token=config.central_auth_token,
1051
+ )
1052
+ username = os.getenv("VDS_USERNAME")
1053
+ password = os.getenv("VDS_PASSWORD")
1054
+ if not auth_token and not (username and password):
1055
+ raise SyncError(
1056
+ "Confluence credentials missing. Provide VDS_USERNAME+VDS_PASSWORD or "
1057
+ "INTERNAL_CONFLUENCE_TOKEN/EXTERNAL_CONFLUENCE_TOKEN (or --central-auth-token)."
1058
+ )
1059
+ return HttpCentralStateBackend(
1060
+ client=ConfluencePropertySyncApiClient(
1061
+ base_url=base_url,
1062
+ page_id=page_id,
1063
+ property_key=config.central_confluence_property_key,
1064
+ timeout_seconds=config.central_timeout_seconds,
1065
+ auth_token=auth_token,
1066
+ username=username,
1067
+ password=password,
1068
+ )
1069
+ )
1070
+ raise SyncError(
1071
+ "Invalid central contract for HTTP backend. Expected: generic, vds-ai-memory, confluence-property."
1072
+ )
1073
+ raise SyncError(
1074
+ f"Invalid central backend '{config.central_backend}'. Expected: local, http."
1075
+ )
1076
+
1077
+
1078
+ def _resolve_confluence_base_url(*, server: str, override: str | None) -> str:
1079
+ if override:
1080
+ return override
1081
+ server_lower = server.lower().strip()
1082
+ if server_lower in {"internal", "int", "main"}:
1083
+ return os.getenv("CONFLUENCE_INTERNAL_URL", "http://confluence.digital.vn")
1084
+ if server_lower in {"external", "ext", "outsource"}:
1085
+ return os.getenv("CONFLUENCE_EXTERNAL_URL", "http://10.254.136.35:8090")
1086
+ raise SyncError(
1087
+ f"Unknown confluence server '{server}'. Expected internal or external."
1088
+ )
1089
+
1090
+
1091
+ def _resolve_confluence_token(*, server: str, explicit_token: str | None) -> str | None:
1092
+ if explicit_token:
1093
+ return explicit_token
1094
+ server_lower = server.lower().strip()
1095
+ if server_lower in {"internal", "int", "main"}:
1096
+ return os.getenv("INTERNAL_CONFLUENCE_TOKEN")
1097
+ if server_lower in {"external", "ext", "outsource"}:
1098
+ return os.getenv("EXTERNAL_CONFLUENCE_TOKEN")
1099
+ return None
1100
+
1101
+
1102
+ def _sha256(content: str) -> str:
1103
+ import hashlib
1104
+
1105
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
1106
+
1107
+
1108
+ def _hash_groups(hashes: dict[str, str]) -> dict[str, list[str]]:
1109
+ grouped: dict[str, list[str]] = {}
1110
+ for relpath, digest in hashes.items():
1111
+ grouped.setdefault(digest, []).append(relpath)
1112
+ return grouped
1113
+
1114
+
1115
+ def _atomic_write_text(path: Path, content: str) -> None:
1116
+ path.parent.mkdir(parents=True, exist_ok=True)
1117
+ temp_path: Path | None = None
1118
+ try:
1119
+ with tempfile.NamedTemporaryFile(
1120
+ mode="w",
1121
+ encoding="utf-8",
1122
+ dir=path.parent,
1123
+ delete=False,
1124
+ prefix=f".{path.name}.",
1125
+ suffix=".tmp",
1126
+ ) as fh:
1127
+ fh.write(content)
1128
+ fh.flush()
1129
+ os.fsync(fh.fileno())
1130
+ temp_path = Path(fh.name)
1131
+
1132
+ for attempt in range(4):
1133
+ try:
1134
+ os.replace(temp_path, path)
1135
+ return
1136
+ except OSError as exc:
1137
+ if not _is_retryable_replace_error(exc) or attempt >= 3:
1138
+ raise
1139
+ time.sleep(0.05 * (attempt + 1))
1140
+ finally:
1141
+ if temp_path is not None and temp_path.exists():
1142
+ temp_path.unlink(missing_ok=True)
1143
+
1144
+
1145
+ def _is_retryable_replace_error(exc: OSError) -> bool:
1146
+ if isinstance(exc, PermissionError):
1147
+ return True
1148
+ if exc.errno in {EACCES, EPERM, EBUSY}:
1149
+ return True
1150
+ return False
1151
+
1152
+
1153
+ def _write_json_atomic(path: Path, payload: dict[str, object]) -> None:
1154
+ _atomic_write_text(path, json.dumps(payload, indent=2, ensure_ascii=True) + "\n")
1155
+
1156
+
1157
+ def _as_central_document(document: SyncApiDocument) -> CentralDocument:
1158
+ return CentralDocument(
1159
+ content=document.content,
1160
+ revision=document.revision,
1161
+ content_hash=document.content_hash,
1162
+ source_file=document.source_file,
1163
+ updated_at=document.updated_at,
1164
+ )
1165
+
1166
+
1167
+ def _load_watchfiles_watch():
1168
+ try:
1169
+ from watchfiles import watch # type: ignore
1170
+ except ModuleNotFoundError:
1171
+ return None
1172
+ return watch
1173
+
1174
+
1175
+ def _line_edits(base_lines: list[str], target_lines: list[str]) -> list[LineEdit]:
1176
+ edits: list[LineEdit] = []
1177
+ matcher = SequenceMatcher(a=base_lines, b=target_lines)
1178
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
1179
+ if tag == "equal":
1180
+ continue
1181
+ edits.append(LineEdit(start=i1, end=i2, replacement=tuple(target_lines[j1:j2])))
1182
+ return edits
1183
+
1184
+
1185
+ def _edits_conflict(left: LineEdit, right: LineEdit) -> bool:
1186
+ if (
1187
+ left.start == right.start
1188
+ and left.end == right.end
1189
+ and left.replacement == right.replacement
1190
+ ):
1191
+ return False
1192
+
1193
+ left_insert = left.start == left.end
1194
+ right_insert = right.start == right.end
1195
+
1196
+ if left_insert and right_insert:
1197
+ return left.start == right.start
1198
+ if left_insert:
1199
+ return right.start <= left.start < right.end
1200
+ if right_insert:
1201
+ return left.start <= right.start < left.end
1202
+
1203
+ return not (left.end <= right.start or right.end <= left.start)