@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,647 @@
1
+ """HTTP central-state clients for instruction sync."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ from dataclasses import dataclass
9
+ from datetime import UTC, datetime
10
+ from urllib.error import HTTPError, URLError
11
+ from urllib.parse import quote
12
+ from urllib.request import Request, urlopen
13
+
14
+
15
+ class SyncApiError(RuntimeError):
16
+ """Base error for central-state HTTP client failures."""
17
+
18
+
19
+ class SyncApiRevisionConflict(SyncApiError):
20
+ """Raised when optimistic concurrency check fails."""
21
+
22
+
23
+ class SyncApiTransientError(SyncApiError):
24
+ """Raised when HTTP/network failures are transient and retryable."""
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class SyncApiDocument:
29
+ """Canonical document representation returned by central-state API."""
30
+
31
+ content: str
32
+ revision: str
33
+ content_hash: str
34
+ source_file: str
35
+ updated_at: str
36
+
37
+
38
+ @dataclass
39
+ class SyncApiClient:
40
+ """Generic HTTP client with If-Match/If-None-Match semantics."""
41
+
42
+ endpoint: str
43
+ timeout_seconds: int = 10
44
+ auth_token: str | None = None
45
+
46
+ def pull_document(self) -> SyncApiDocument | None:
47
+ request = Request(
48
+ self.endpoint,
49
+ headers=self._auth_headers(),
50
+ method="GET",
51
+ )
52
+ try:
53
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
54
+ payload = self._read_json_response(response)
55
+ if payload is None:
56
+ return None
57
+ revision = _normalize_revision(
58
+ etag=response.headers.get("ETag"),
59
+ payload_revision=payload.get("revision"),
60
+ )
61
+ return _payload_to_document(payload, revision=revision)
62
+ except HTTPError as exc:
63
+ if exc.code == 404:
64
+ return None
65
+ if _is_transient_http_code(exc.code):
66
+ raise SyncApiTransientError(
67
+ f"Transient HTTP {exc.code} when pulling central document."
68
+ ) from exc
69
+ raise SyncApiError(f"HTTP {exc.code} when pulling central document.") from exc
70
+ except URLError as exc:
71
+ raise SyncApiTransientError(
72
+ f"Network error pulling central document: {exc.reason}"
73
+ ) from exc
74
+
75
+ def push_document_if_match(
76
+ self,
77
+ *,
78
+ content: str,
79
+ source_file: str,
80
+ expected_revision: str | None,
81
+ ) -> SyncApiDocument:
82
+ headers = {
83
+ "Content-Type": "application/json",
84
+ **self._auth_headers(),
85
+ }
86
+ if expected_revision is None:
87
+ headers["If-None-Match"] = "*"
88
+ else:
89
+ headers["If-Match"] = _format_etag(expected_revision)
90
+
91
+ payload = {
92
+ "content": content,
93
+ "source_file": source_file,
94
+ "content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
95
+ "updated_at": datetime.now(UTC).isoformat(),
96
+ }
97
+
98
+ request = Request(
99
+ self.endpoint,
100
+ data=json.dumps(payload).encode("utf-8"),
101
+ headers=headers,
102
+ method="PUT",
103
+ )
104
+ try:
105
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
106
+ response_payload = self._read_json_response(response)
107
+ if response_payload is None:
108
+ document = self.pull_document()
109
+ if document is None:
110
+ raise SyncApiError(
111
+ "Central API returned empty PUT response and GET could not load canonical document."
112
+ ) from None
113
+ return document
114
+ revision = _normalize_revision(
115
+ etag=response.headers.get("ETag"),
116
+ payload_revision=response_payload.get("revision"),
117
+ )
118
+ return _payload_to_document(response_payload, revision=revision)
119
+ except HTTPError as exc:
120
+ if exc.code == 412:
121
+ raise SyncApiRevisionConflict("Central revision mismatch (HTTP 412).") from exc
122
+ if _is_transient_http_code(exc.code):
123
+ raise SyncApiTransientError(
124
+ f"Transient HTTP {exc.code} when pushing central document."
125
+ ) from exc
126
+ raise SyncApiError(f"HTTP {exc.code} when pushing central document.") from exc
127
+ except URLError as exc:
128
+ raise SyncApiTransientError(
129
+ f"Network error pushing central document: {exc.reason}"
130
+ ) from exc
131
+
132
+ def _auth_headers(self) -> dict[str, str]:
133
+ if not self.auth_token:
134
+ return {}
135
+ return {"Authorization": f"Bearer {self.auth_token}"}
136
+
137
+ @staticmethod
138
+ def _read_json_response(response) -> dict[str, object] | None:
139
+ raw = response.read()
140
+ if not raw:
141
+ return None
142
+ try:
143
+ loaded = json.loads(raw.decode("utf-8"))
144
+ except json.JSONDecodeError as exc:
145
+ raise SyncApiError("Central API returned non-JSON response payload.") from exc
146
+ if not isinstance(loaded, dict):
147
+ raise SyncApiError("Central API payload must be a JSON object.")
148
+ return loaded
149
+
150
+
151
+ @dataclass
152
+ class VdsAiMemorySyncApiClient:
153
+ """VDS AI Memory contract adapter (`/api/v1/memories/{memory_id}`)."""
154
+
155
+ base_url: str
156
+ memory_id: str
157
+ user_id: str
158
+ session_id: str
159
+ timeout_seconds: int = 10
160
+ auth_token: str | None = None
161
+
162
+ def pull_document(self) -> SyncApiDocument | None:
163
+ request = Request(self._memory_url, headers=self._auth_headers(), method="GET")
164
+ try:
165
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
166
+ payload = SyncApiClient._read_json_response(response)
167
+ if payload is None:
168
+ return None
169
+ return _memory_payload_to_document(payload)
170
+ except HTTPError as exc:
171
+ if exc.code == 404:
172
+ return None
173
+ if _is_transient_http_code(exc.code):
174
+ raise SyncApiTransientError(
175
+ f"Transient HTTP {exc.code} when pulling VDS AI Memory document."
176
+ ) from exc
177
+ raise SyncApiError(f"HTTP {exc.code} when pulling VDS AI Memory document.") from exc
178
+ except URLError as exc:
179
+ raise SyncApiTransientError(
180
+ f"Network error pulling VDS AI Memory document: {exc.reason}"
181
+ ) from exc
182
+
183
+ def push_document_if_match(
184
+ self,
185
+ *,
186
+ content: str,
187
+ source_file: str,
188
+ expected_revision: str | None,
189
+ ) -> SyncApiDocument:
190
+ current = self.pull_document()
191
+ if current is None:
192
+ if expected_revision is not None:
193
+ raise SyncApiRevisionConflict(
194
+ "Central VDS AI Memory record missing while expected revision was provided."
195
+ )
196
+ return self._create_memory(content=content, source_file=source_file)
197
+
198
+ if expected_revision is not None and current.revision != expected_revision:
199
+ raise SyncApiRevisionConflict(
200
+ f"Central revision mismatch (expected={expected_revision}, actual={current.revision})."
201
+ )
202
+
203
+ expected_revision_number = (
204
+ _parse_int_revision(expected_revision)
205
+ if expected_revision is not None
206
+ else None
207
+ )
208
+ if expected_revision is not None and expected_revision_number is None:
209
+ raise SyncApiError(
210
+ "VDS AI Memory expected revision must be an integer current_version_number."
211
+ )
212
+
213
+ payload = {
214
+ "content": content,
215
+ "metadata": _sync_metadata(
216
+ source_file=source_file,
217
+ content=content,
218
+ ),
219
+ "user_id": self.user_id,
220
+ "session_id": self.session_id,
221
+ }
222
+ headers = {
223
+ "Content-Type": "application/json",
224
+ **self._auth_headers(),
225
+ }
226
+ if expected_revision_number is not None:
227
+ payload["expected_current_version_number"] = expected_revision_number
228
+ headers["If-Match"] = _format_etag(str(expected_revision_number))
229
+
230
+ request = Request(
231
+ self._memory_url,
232
+ data=json.dumps(payload).encode("utf-8"),
233
+ headers=headers,
234
+ method="PATCH",
235
+ )
236
+ try:
237
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
238
+ patch_payload = SyncApiClient._read_json_response(response)
239
+ if patch_payload is not None:
240
+ return _memory_payload_to_document(patch_payload)
241
+ refreshed = self.pull_document()
242
+ if refreshed is None:
243
+ raise SyncApiError("VDS AI Memory patch succeeded but follow-up GET returned empty response.")
244
+ return refreshed
245
+ except HTTPError as exc:
246
+ if exc.code == 409:
247
+ raise SyncApiRevisionConflict("Central revision mismatch (HTTP 409).") from exc
248
+ if _is_transient_http_code(exc.code):
249
+ raise SyncApiTransientError(
250
+ f"Transient HTTP {exc.code} when pushing VDS AI Memory document."
251
+ ) from exc
252
+ raise SyncApiError(f"HTTP {exc.code} when pushing VDS AI Memory document.") from exc
253
+ except URLError as exc:
254
+ raise SyncApiTransientError(
255
+ f"Network error pushing VDS AI Memory document: {exc.reason}"
256
+ ) from exc
257
+
258
+ @property
259
+ def _api_base(self) -> str:
260
+ base = self.base_url.rstrip("/")
261
+ if base.endswith("/api/v1"):
262
+ return base
263
+ return f"{base}/api/v1"
264
+
265
+ @property
266
+ def _memory_url(self) -> str:
267
+ return f"{self._api_base}/memories/{quote(self.memory_id, safe='')}"
268
+
269
+ @property
270
+ def _create_url(self) -> str:
271
+ return f"{self._api_base}/memories"
272
+
273
+ def _create_memory(self, *, content: str, source_file: str) -> SyncApiDocument:
274
+ payload = {
275
+ "content": content,
276
+ "category": "reference",
277
+ "importance": "medium",
278
+ "user_id": self.user_id,
279
+ "session_id": self.session_id,
280
+ "metadata": _sync_metadata(source_file=source_file, content=content),
281
+ }
282
+ request = Request(
283
+ self._create_url,
284
+ data=json.dumps(payload).encode("utf-8"),
285
+ headers={
286
+ "Content-Type": "application/json",
287
+ **self._auth_headers(),
288
+ },
289
+ method="POST",
290
+ )
291
+ try:
292
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
293
+ create_payload = SyncApiClient._read_json_response(response)
294
+ except HTTPError as exc:
295
+ if _is_transient_http_code(exc.code):
296
+ raise SyncApiTransientError(
297
+ f"Transient HTTP {exc.code} when creating VDS AI Memory record."
298
+ ) from exc
299
+ raise SyncApiError(f"HTTP {exc.code} when creating VDS AI Memory record.") from exc
300
+ except URLError as exc:
301
+ raise SyncApiTransientError(
302
+ f"Network error creating VDS AI Memory record: {exc.reason}"
303
+ ) from exc
304
+
305
+ if create_payload is None:
306
+ raise SyncApiError("VDS AI Memory create response was empty.")
307
+ created_id = create_payload.get("memory_id")
308
+ if not isinstance(created_id, str) or not created_id.strip():
309
+ raise SyncApiError("VDS AI Memory create response missing memory_id.")
310
+ self.memory_id = created_id
311
+ created = self.pull_document()
312
+ if created is None:
313
+ raise SyncApiError("VDS AI Memory create succeeded but follow-up GET failed.")
314
+ return created
315
+
316
+ def _auth_headers(self) -> dict[str, str]:
317
+ if not self.auth_token:
318
+ return {}
319
+ return {"Authorization": f"Bearer {self.auth_token}"}
320
+
321
+
322
+ @dataclass
323
+ class ConfluencePropertySyncApiClient:
324
+ """Confluence content-property contract adapter."""
325
+
326
+ base_url: str
327
+ page_id: str
328
+ property_key: str
329
+ timeout_seconds: int = 10
330
+ auth_token: str | None = None
331
+ username: str | None = None
332
+ password: str | None = None
333
+
334
+ def pull_document(self) -> SyncApiDocument | None:
335
+ request = Request(self._property_url, headers=self._auth_headers(), method="GET")
336
+ try:
337
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
338
+ payload = SyncApiClient._read_json_response(response)
339
+ if payload is None:
340
+ return None
341
+ return _confluence_property_payload_to_document(payload)
342
+ except HTTPError as exc:
343
+ if exc.code == 404:
344
+ return None
345
+ if _is_transient_http_code(exc.code):
346
+ raise SyncApiTransientError(
347
+ f"Transient HTTP {exc.code} when pulling Confluence sync property."
348
+ ) from exc
349
+ raise SyncApiError(f"HTTP {exc.code} when pulling Confluence sync property.") from exc
350
+ except URLError as exc:
351
+ raise SyncApiTransientError(
352
+ f"Network error pulling Confluence sync property: {exc.reason}"
353
+ ) from exc
354
+
355
+ def push_document_if_match(
356
+ self,
357
+ *,
358
+ content: str,
359
+ source_file: str,
360
+ expected_revision: str | None,
361
+ ) -> SyncApiDocument:
362
+ current = self.pull_document()
363
+ if expected_revision is None:
364
+ if current is not None:
365
+ raise SyncApiRevisionConflict(
366
+ f"Expected empty Confluence property but found revision {current.revision}."
367
+ )
368
+ return self._create_property(content=content, source_file=source_file)
369
+
370
+ if current is None:
371
+ raise SyncApiRevisionConflict(
372
+ "Expected existing Confluence property revision but property was missing."
373
+ )
374
+ if current.revision != expected_revision:
375
+ raise SyncApiRevisionConflict(
376
+ f"Central revision mismatch (expected={expected_revision}, actual={current.revision})."
377
+ )
378
+
379
+ next_version = _next_revision_number(current.revision)
380
+ payload = {
381
+ "key": self.property_key,
382
+ "value": _sync_payload_value(
383
+ source_file=source_file,
384
+ content=content,
385
+ ),
386
+ "version": {"number": next_version},
387
+ }
388
+ request = Request(
389
+ self._property_url,
390
+ data=json.dumps(payload).encode("utf-8"),
391
+ headers={
392
+ "Content-Type": "application/json",
393
+ **self._auth_headers(),
394
+ },
395
+ method="PUT",
396
+ )
397
+ try:
398
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
399
+ response_payload = SyncApiClient._read_json_response(response)
400
+ if response_payload is None:
401
+ refreshed = self.pull_document()
402
+ if refreshed is None:
403
+ raise SyncApiError(
404
+ "Confluence sync property update succeeded but follow-up GET failed."
405
+ ) from None
406
+ return refreshed
407
+ return _confluence_property_payload_to_document(response_payload)
408
+ except HTTPError as exc:
409
+ if exc.code in {409, 412}:
410
+ raise SyncApiRevisionConflict("Central revision mismatch from Confluence.") from exc
411
+ if _is_transient_http_code(exc.code):
412
+ raise SyncApiTransientError(
413
+ f"Transient HTTP {exc.code} when pushing Confluence sync property."
414
+ ) from exc
415
+ raise SyncApiError(f"HTTP {exc.code} when pushing Confluence sync property.") from exc
416
+ except URLError as exc:
417
+ raise SyncApiTransientError(
418
+ f"Network error pushing Confluence sync property: {exc.reason}"
419
+ ) from exc
420
+
421
+ @property
422
+ def _property_url(self) -> str:
423
+ return (
424
+ f"{self.base_url.rstrip('/')}/rest/api/content/{quote(self.page_id, safe='')}/property/"
425
+ f"{quote(self.property_key, safe='')}"
426
+ )
427
+
428
+ @property
429
+ def _create_url(self) -> str:
430
+ return f"{self.base_url.rstrip('/')}/rest/api/content/{quote(self.page_id, safe='')}/property"
431
+
432
+ def _create_property(self, *, content: str, source_file: str) -> SyncApiDocument:
433
+ payload = {
434
+ "key": self.property_key,
435
+ "value": _sync_payload_value(source_file=source_file, content=content),
436
+ }
437
+ request = Request(
438
+ self._create_url,
439
+ data=json.dumps(payload).encode("utf-8"),
440
+ headers={
441
+ "Content-Type": "application/json",
442
+ **self._auth_headers(),
443
+ },
444
+ method="POST",
445
+ )
446
+ try:
447
+ with urlopen(request, timeout=self.timeout_seconds) as response: # noqa: S310
448
+ response_payload = SyncApiClient._read_json_response(response)
449
+ except HTTPError as exc:
450
+ if exc.code in {409, 412}:
451
+ raise SyncApiRevisionConflict("Confluence property create conflict.") from exc
452
+ if _is_transient_http_code(exc.code):
453
+ raise SyncApiTransientError(
454
+ f"Transient HTTP {exc.code} when creating Confluence sync property."
455
+ ) from exc
456
+ raise SyncApiError(f"HTTP {exc.code} when creating Confluence sync property.") from exc
457
+ except URLError as exc:
458
+ raise SyncApiTransientError(
459
+ f"Network error creating Confluence sync property: {exc.reason}"
460
+ ) from exc
461
+
462
+ if response_payload is None:
463
+ refreshed = self.pull_document()
464
+ if refreshed is None:
465
+ raise SyncApiError("Confluence property create succeeded but follow-up GET failed.")
466
+ return refreshed
467
+ return _confluence_property_payload_to_document(response_payload)
468
+
469
+ def _auth_headers(self) -> dict[str, str]:
470
+ if self.auth_token:
471
+ return {"Authorization": f"Bearer {self.auth_token}"}
472
+ if self.username and self.password:
473
+ token = base64.b64encode(f"{self.username}:{self.password}".encode()).decode("ascii")
474
+ return {"Authorization": f"Basic {token}"}
475
+ return {}
476
+
477
+
478
+ def _format_etag(revision: str) -> str:
479
+ candidate = revision.strip()
480
+ if candidate.startswith('"') and candidate.endswith('"'):
481
+ return candidate
482
+ return f'"{candidate}"'
483
+
484
+
485
+ def _normalize_revision(*, etag: str | None, payload_revision: object | None) -> str:
486
+ if etag:
487
+ candidate = etag.strip()
488
+ if candidate.startswith("W/"):
489
+ candidate = candidate[2:].strip()
490
+ if candidate.startswith('"') and candidate.endswith('"') and len(candidate) >= 2:
491
+ candidate = candidate[1:-1]
492
+ if candidate:
493
+ return candidate
494
+ if payload_revision is not None:
495
+ value = str(payload_revision).strip()
496
+ if value:
497
+ return value
498
+ raise SyncApiError("Central API response missing revision/ETag.")
499
+
500
+
501
+ def _payload_to_document(payload: dict[str, object], *, revision: str) -> SyncApiDocument:
502
+ content = _first_str(payload, ("content", "document_body", "body"))
503
+ if content is None:
504
+ raise SyncApiError("Central API payload missing content field.")
505
+ content_hash = _first_str(payload, ("content_hash",))
506
+ if content_hash is None:
507
+ content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
508
+ source_file = _first_str(payload, ("source_file",))
509
+ if source_file is None:
510
+ source_file = "<remote>"
511
+ updated_at = _first_str(payload, ("updated_at",))
512
+ if updated_at is None:
513
+ updated_at = datetime.now(UTC).isoformat()
514
+ return SyncApiDocument(
515
+ content=content,
516
+ revision=revision,
517
+ content_hash=content_hash,
518
+ source_file=source_file,
519
+ updated_at=updated_at,
520
+ )
521
+
522
+
523
+ def _memory_payload_to_document(payload: dict[str, object]) -> SyncApiDocument:
524
+ content = _first_str(payload, ("content",))
525
+ if content is None:
526
+ raise SyncApiError("VDS AI Memory payload missing content field.")
527
+
528
+ metadata = payload.get("metadata")
529
+ metadata_dict: dict[str, object] = metadata if isinstance(metadata, dict) else {}
530
+
531
+ revision_obj = payload.get("current_version_number")
532
+ if revision_obj is None:
533
+ revision_obj = payload.get("updated_at")
534
+ if revision_obj is None:
535
+ raise SyncApiError("VDS AI Memory payload missing revision field.")
536
+ revision = str(revision_obj)
537
+
538
+ source_file = _first_str(metadata_dict, ("sync_source_file",))
539
+ if source_file is None:
540
+ source_file = _first_str(payload, ("source_file",))
541
+ if source_file is None:
542
+ source_file = "<vds-ai-memory>"
543
+
544
+ content_hash = _first_str(metadata_dict, ("sync_content_hash",))
545
+ if content_hash is None:
546
+ content_hash = _first_str(payload, ("content_hash",))
547
+ if content_hash is None:
548
+ content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
549
+
550
+ updated_at = _first_str(payload, ("updated_at",))
551
+ if updated_at is None:
552
+ updated_at = _first_str(metadata_dict, ("sync_updated_at",))
553
+ if updated_at is None:
554
+ updated_at = datetime.now(UTC).isoformat()
555
+
556
+ return SyncApiDocument(
557
+ content=content,
558
+ revision=revision,
559
+ content_hash=content_hash,
560
+ source_file=source_file,
561
+ updated_at=updated_at,
562
+ )
563
+
564
+
565
+ def _confluence_property_payload_to_document(payload: dict[str, object]) -> SyncApiDocument:
566
+ value = payload.get("value")
567
+ value_dict: dict[str, object] = value if isinstance(value, dict) else {}
568
+
569
+ content = _first_str(value_dict, ("content", "document_body", "body"))
570
+ if content is None and isinstance(value, str):
571
+ content = value
572
+ if content is None:
573
+ raise SyncApiError("Confluence property payload missing content value.")
574
+
575
+ version_payload = payload.get("version")
576
+ version_dict: dict[str, object] = version_payload if isinstance(version_payload, dict) else {}
577
+ revision_obj = version_dict.get("number")
578
+ if revision_obj is None:
579
+ raise SyncApiError("Confluence property payload missing version.number.")
580
+ revision = str(revision_obj)
581
+
582
+ source_file = _first_str(value_dict, ("source_file",))
583
+ if source_file is None:
584
+ source_file = "<confluence-property>"
585
+
586
+ content_hash = _first_str(value_dict, ("content_hash",))
587
+ if content_hash is None:
588
+ content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
589
+
590
+ updated_at = _first_str(value_dict, ("updated_at",))
591
+ if updated_at is None:
592
+ updated_at = _first_str(version_dict, ("when",))
593
+ if updated_at is None:
594
+ updated_at = datetime.now(UTC).isoformat()
595
+
596
+ return SyncApiDocument(
597
+ content=content,
598
+ revision=revision,
599
+ content_hash=content_hash,
600
+ source_file=source_file,
601
+ updated_at=updated_at,
602
+ )
603
+
604
+
605
+ def _sync_metadata(*, source_file: str, content: str) -> dict[str, str]:
606
+ return {
607
+ "sync_source_file": source_file,
608
+ "sync_content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
609
+ "sync_updated_at": datetime.now(UTC).isoformat(),
610
+ }
611
+
612
+
613
+ def _sync_payload_value(*, source_file: str, content: str) -> dict[str, str]:
614
+ return {
615
+ "content": content,
616
+ "source_file": source_file,
617
+ "content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
618
+ "updated_at": datetime.now(UTC).isoformat(),
619
+ }
620
+
621
+
622
+ def _next_revision_number(revision: str) -> int:
623
+ try:
624
+ return int(revision) + 1
625
+ except ValueError as exc:
626
+ raise SyncApiError(f"Confluence revision is not an integer: {revision}") from exc
627
+
628
+
629
+ def _first_str(payload: dict[str, object], keys: tuple[str, ...]) -> str | None:
630
+ for key in keys:
631
+ value = payload.get(key)
632
+ if isinstance(value, str):
633
+ return value
634
+ return None
635
+
636
+
637
+ def _parse_int_revision(revision: str | None) -> int | None:
638
+ if revision is None:
639
+ return None
640
+ try:
641
+ return int(revision)
642
+ except ValueError:
643
+ return None
644
+
645
+
646
+ def _is_transient_http_code(code: int) -> bool:
647
+ return code in {408, 429} or 500 <= code < 600