ummaya 0.2.4 → 0.2.6

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 (482) hide show
  1. package/README.md +15 -2
  2. package/bin/ummaya +10 -1
  3. package/bun.lock +180 -244
  4. package/npm-shrinkwrap.json +760 -1760
  5. package/package.json +39 -22
  6. package/prompts/manifest.yaml +1 -1
  7. package/prompts/system_v1.md +1 -0
  8. package/pyproject.toml +27 -2
  9. package/specs/2803-document-production-hardening/contracts/document-tools.schema.json +1043 -0
  10. package/src/ummaya/_canonical/__init__.py +2 -0
  11. package/src/ummaya/_canonical/baselines.yaml +113 -0
  12. package/src/ummaya/engine/engine.py +29 -132
  13. package/src/ummaya/evidence/__init__.py +21 -2
  14. package/src/ummaya/evidence/dataset_contract.py +193 -0
  15. package/src/ummaya/evidence/document_authoring_cases.py +33 -0
  16. package/src/ummaya/evidence/document_harness.py +313 -0
  17. package/src/ummaya/evidence/document_viewer_ux.py +391 -0
  18. package/src/ummaya/evidence/gates.py +70 -0
  19. package/src/ummaya/evidence/json_types.py +20 -0
  20. package/src/ummaya/evidence/models.py +88 -1
  21. package/src/ummaya/evidence/output_payload.py +89 -0
  22. package/src/ummaya/evidence/payload_documents.py +233 -0
  23. package/src/ummaya/evidence/route_contracts.py +224 -0
  24. package/src/ummaya/evidence/route_helpers.py +150 -0
  25. package/src/ummaya/evidence/runner.py +81 -212
  26. package/src/ummaya/evidence/source_provenance.py +246 -0
  27. package/src/ummaya/evidence/source_provenance_redaction.py +176 -0
  28. package/src/ummaya/evidence/tool_layer.py +39 -0
  29. package/src/ummaya/evidence/tool_layer_models.py +151 -0
  30. package/src/ummaya/ipc/adapter_manifest_emitter.py +26 -10
  31. package/src/ummaya/ipc/document_intent_normalization.py +185 -0
  32. package/src/ummaya/ipc/frame_schema.py +5 -5
  33. package/src/ummaya/ipc/route_diagnostics.py +73 -0
  34. package/src/ummaya/ipc/stdio.py +1109 -477
  35. package/src/ummaya/llm/client.py +102 -3
  36. package/src/ummaya/llm/config.py +8 -3
  37. package/src/ummaya/primitives/__init__.py +6 -2
  38. package/src/ummaya/primitives/delegation.py +1 -1
  39. package/src/ummaya/primitives/document.py +28 -0
  40. package/src/ummaya/settings.py +0 -3
  41. package/src/ummaya/tools/discovery_bridge.py +17 -1
  42. package/src/ummaya/tools/documents/__init__.py +297 -0
  43. package/src/ummaya/tools/documents/adapter_registry.py +487 -0
  44. package/src/ummaya/tools/documents/archive_container_probe.py +167 -0
  45. package/src/ummaya/tools/documents/artifact_store.py +454 -0
  46. package/src/ummaya/tools/documents/authoring.py +283 -0
  47. package/src/ummaya/tools/documents/baselines.py +132 -0
  48. package/src/ummaya/tools/documents/capability.py +331 -0
  49. package/src/ummaya/tools/documents/contracts.py +112 -0
  50. package/src/ummaya/tools/documents/conversion.py +521 -0
  51. package/src/ummaya/tools/documents/diff.py +275 -0
  52. package/src/ummaya/tools/documents/engines.py +163 -0
  53. package/src/ummaya/tools/documents/evaluation.py +291 -0
  54. package/src/ummaya/tools/documents/explicit_values.py +108 -0
  55. package/src/ummaya/tools/documents/fixtures.py +174 -0
  56. package/src/ummaya/tools/documents/format_completion_audit.py +471 -0
  57. package/src/ummaya/tools/documents/formats/__init__.py +2 -0
  58. package/src/ummaya/tools/documents/formats/archive.py +528 -0
  59. package/src/ummaya/tools/documents/formats/base.py +41 -0
  60. package/src/ummaya/tools/documents/formats/code_file.py +211 -0
  61. package/src/ummaya/tools/documents/formats/data_file.py +272 -0
  62. package/src/ummaya/tools/documents/formats/hwp.py +284 -0
  63. package/src/ummaya/tools/documents/formats/hwpx.py +1837 -0
  64. package/src/ummaya/tools/documents/formats/odf.py +435 -0
  65. package/src/ummaya/tools/documents/formats/ooxml.py +1030 -0
  66. package/src/ummaya/tools/documents/formats/passive.py +766 -0
  67. package/src/ummaya/tools/documents/formats/pdf.py +702 -0
  68. package/src/ummaya/tools/documents/formats/text_web.py +268 -0
  69. package/src/ummaya/tools/documents/hwp_conversion_probe.py +178 -0
  70. package/src/ummaya/tools/documents/hwp_direct_candidate.py +141 -0
  71. package/src/ummaya/tools/documents/inspection.py +289 -0
  72. package/src/ummaya/tools/documents/intake.py +1079 -0
  73. package/src/ummaya/tools/documents/legacy_office_promotion_probe.py +366 -0
  74. package/src/ummaya/tools/documents/models.py +1598 -0
  75. package/src/ummaya/tools/documents/odf_promotion_probe.py +167 -0
  76. package/src/ummaya/tools/documents/orchestrator.py +96 -0
  77. package/src/ummaya/tools/documents/passive_capability_probe.py +251 -0
  78. package/src/ummaya/tools/documents/patch.py +170 -0
  79. package/src/ummaya/tools/documents/pdfa_conformance.py +284 -0
  80. package/src/ummaya/tools/documents/pdfa_promotion_probe.py +198 -0
  81. package/src/ummaya/tools/documents/permissions.py +110 -0
  82. package/src/ummaya/tools/documents/planner.py +616 -0
  83. package/src/ummaya/tools/documents/registry.py +2733 -0
  84. package/src/ummaya/tools/documents/render.py +978 -0
  85. package/src/ummaya/tools/documents/render_comparison.py +113 -0
  86. package/src/ummaya/tools/documents/render_comparison_models.py +74 -0
  87. package/src/ummaya/tools/documents/render_comparison_regions.py +73 -0
  88. package/src/ummaya/tools/documents/render_comparison_style.py +161 -0
  89. package/src/ummaya/tools/documents/reread.py +157 -0
  90. package/src/ummaya/tools/documents/runtime_authoring.py +244 -0
  91. package/src/ummaya/tools/documents/runtime_authoring_bundle.py +76 -0
  92. package/src/ummaya/tools/documents/scorecard.py +184 -0
  93. package/src/ummaya/tools/documents/socratic_planner.py +193 -0
  94. package/src/ummaya/tools/documents/style.py +48 -0
  95. package/src/ummaya/tools/documents/tool_defs.py +523 -0
  96. package/src/ummaya/tools/documents/validate.py +347 -0
  97. package/src/ummaya/tools/executor.py +29 -0
  98. package/src/ummaya/tools/live_proxy.py +0 -3
  99. package/src/ummaya/tools/models.py +5 -1
  100. package/src/ummaya/tools/register_all.py +8 -0
  101. package/src/ummaya/tools/registry.py +10 -1
  102. package/src/ummaya/tools/routing/__init__.py +59 -0
  103. package/src/ummaya/tools/routing/builder.py +105 -0
  104. package/src/ummaya/tools/routing/cards.py +29 -0
  105. package/src/ummaya/tools/routing/decision_service.py +534 -0
  106. package/src/ummaya/tools/routing/decision_types.py +74 -0
  107. package/src/ummaya/tools/routing/feasibility.py +122 -0
  108. package/src/ummaya/tools/routing/intent.py +17 -0
  109. package/src/ummaya/tools/routing/intent_extractor.py +207 -0
  110. package/src/ummaya/tools/routing/intent_patterns.py +160 -0
  111. package/src/ummaya/tools/routing/intent_public_data.py +150 -0
  112. package/src/ummaya/tools/routing/intent_types.py +48 -0
  113. package/src/ummaya/tools/routing/lint.py +78 -0
  114. package/src/ummaya/tools/routing/metadata.py +174 -0
  115. package/src/ummaya/tools/routing/projection.py +340 -0
  116. package/src/ummaya/tools/routing/retrieval_policy.py +629 -0
  117. package/src/ummaya/tools/routing/schema.py +81 -0
  118. package/src/ummaya/tools/routing/types.py +96 -0
  119. package/src/ummaya/tools/routing_index.py +2 -2
  120. package/src/ummaya/tools/search.py +34 -746
  121. package/tests/fixtures/documents/public_forms/baselines.yaml +113 -0
  122. package/tui/bun.lock +126 -305
  123. package/tui/package.json +35 -22
  124. package/tui/src/.cc-byte-identical-whitelist.yaml +266 -0
  125. package/tui/src/QueryEngine.ts +12 -8
  126. package/tui/src/bridge/inboundAttachments.ts +3 -3
  127. package/tui/src/cli/handlers/auth.ts +3 -12
  128. package/tui/src/cli/handlers/mcp.tsx +0 -1
  129. package/tui/src/cli/print.ts +8 -9
  130. package/tui/src/commands/insights.ts +1 -1
  131. package/tui/src/commands/install-github-app/types.ts +8 -30
  132. package/tui/src/commands/plugin/types.ts +6 -28
  133. package/tui/src/commands/plugin/unifiedTypes.ts +4 -26
  134. package/tui/src/commands/rename/generateSessionName.ts +1 -1
  135. package/tui/src/components/Feedback.tsx +1 -1
  136. package/tui/src/components/LogoV2/EmergencyTip.tsx +11 -2
  137. package/tui/src/components/LogoV2/WelcomeV2.tsx +1 -3
  138. package/tui/src/components/ScrollKeybindingHandler.tsx +6 -6
  139. package/tui/src/components/Spinner/types.ts +6 -28
  140. package/tui/src/components/agents/generateAgent.ts +1 -1
  141. package/tui/src/components/agents/new-agent-creation/types.ts +4 -26
  142. package/tui/src/components/config/EnvSecretIsolatedEditor.tsx +1 -1
  143. package/tui/src/components/mcp/types.ts +16 -38
  144. package/tui/src/components/messages/AssistantToolUseMessage.tsx +3 -2
  145. package/tui/src/components/messages/UserCrossSessionMessage.ts +16 -4
  146. package/tui/src/components/messages/UserForkBoilerplateMessage.ts +16 -4
  147. package/tui/src/components/messages/UserGitHubWebhookMessage.ts +16 -4
  148. package/tui/src/components/messages/UserToolResultMessage/utils.tsx +3 -2
  149. package/tui/src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.ts +9 -4
  150. package/tui/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.ts +9 -4
  151. package/tui/src/components/primitive/DocumentSocraticReviewBlock.tsx +129 -0
  152. package/tui/src/components/primitive/DocumentToolResultCard.tsx +224 -0
  153. package/tui/src/components/primitive/documentSocraticReview.ts +215 -0
  154. package/tui/src/components/primitive/index.tsx +43 -1
  155. package/tui/src/components/primitive/types.ts +137 -0
  156. package/tui/src/components/ui/option.ts +4 -26
  157. package/tui/src/constants/common.ts +0 -2
  158. package/tui/src/constants/prompts.ts +4 -3
  159. package/tui/src/constants/querySource.ts +4 -26
  160. package/tui/src/entrypoints/sdk/controlTypes.ts +26 -48
  161. package/tui/src/entrypoints/sdk/coreTypes.generated.ts +3 -25
  162. package/tui/src/entrypoints/sdk/runtimeTypes.ts +38 -60
  163. package/tui/src/entrypoints/sdk/sdkUtilityTypes.ts +4 -26
  164. package/tui/src/entrypoints/sdk/settingsTypes.generated.ts +3 -25
  165. package/tui/src/entrypoints/sdk/toolTypes.ts +3 -25
  166. package/tui/src/hooks/toolPermission/handlers/interactiveHandler.ts +10 -0
  167. package/tui/src/hooks/useApiKeyVerification.ts +1 -1
  168. package/tui/src/hooks/useVirtualScroll.ts +1 -1
  169. package/tui/src/ink/ink.tsx +33 -14
  170. package/tui/src/ink/reconciler.ts +2 -3
  171. package/tui/src/ink/render-to-screen.ts +30 -10
  172. package/tui/src/ipc/bridge.ts +62 -15
  173. package/tui/src/ipc/bridgeSingleton.ts +5 -1
  174. package/tui/src/ipc/codec.ts +3 -3
  175. package/tui/src/ipc/frames.generated.ts +12 -12
  176. package/tui/src/ipc/llmClient.ts +151 -27
  177. package/tui/src/ipc/schema/frame.schema.json +1 -1
  178. package/tui/src/keybindings/defaultBindings.ts +4 -0
  179. package/tui/src/main.tsx +32 -15
  180. package/tui/src/native-ts/file-index/index.ts +33 -3
  181. package/tui/src/observability/surface.ts +2 -2
  182. package/tui/src/probes/toolRegistryProbe.tsx +3 -1
  183. package/tui/src/projectOnboardingState.ts +7 -6
  184. package/tui/src/query/chatMessageTypes.ts +18 -0
  185. package/tui/src/query/chatMessagesBuilder.ts +1 -1
  186. package/tui/src/query/deps.ts +1 -1
  187. package/tui/src/query/messageGuards.ts +106 -0
  188. package/tui/src/query/publicDataTerminalRepair.ts +384 -0
  189. package/tui/src/query/run.ts +1075 -0
  190. package/tui/src/query/supportBoundary.ts +168 -0
  191. package/tui/src/query/toolResultErrors.ts +103 -0
  192. package/tui/src/query/toolRunner.ts +687 -0
  193. package/tui/src/query/unavailableToolRepair.ts +118 -0
  194. package/tui/src/query.ts +9 -2186
  195. package/tui/src/screens/REPL.tsx +40 -29
  196. package/tui/src/services/api/adapterManifest.ts +4 -0
  197. package/tui/src/services/api/backendChat/events.ts +117 -0
  198. package/tui/src/services/api/backendChat/finalMessage.ts +40 -0
  199. package/tui/src/services/api/backendChat/frame.ts +9 -0
  200. package/tui/src/services/api/backendChat/streaming.ts +430 -0
  201. package/tui/src/services/api/backendChat/types.ts +62 -0
  202. package/tui/src/services/api/backendChat.ts +1 -0
  203. package/tui/src/services/api/client.ts +65 -2
  204. package/tui/src/services/api/errorUtils.ts +5 -5
  205. package/tui/src/services/api/errors.ts +1 -1
  206. package/tui/src/services/api/logging.ts +1 -1
  207. package/tui/src/services/api/ummaya/evidence.ts +194 -0
  208. package/tui/src/services/api/ummaya/messages.ts +255 -0
  209. package/tui/src/services/api/ummaya/nonStreaming.ts +66 -0
  210. package/tui/src/services/api/ummaya/provider.ts +200 -0
  211. package/tui/src/services/api/ummaya/reasoning.ts +24 -0
  212. package/tui/src/services/api/ummaya/request.ts +200 -0
  213. package/tui/src/services/api/ummaya/selectionContext.ts +240 -0
  214. package/tui/src/services/api/ummaya/streaming.ts +365 -0
  215. package/tui/src/services/api/ummaya/streamingPayload.ts +129 -0
  216. package/tui/src/services/api/ummaya/streamingReader.ts +40 -0
  217. package/tui/src/services/api/ummaya/toolSelection.ts +217 -0
  218. package/tui/src/services/api/ummaya/types.ts +110 -0
  219. package/tui/src/services/api/ummaya/usage.ts +30 -0
  220. package/tui/src/services/api/ummaya.ts +26 -418
  221. package/tui/src/services/api/withRetry.ts +1 -1
  222. package/tui/src/services/awaySummary.ts +2 -2
  223. package/tui/src/services/claudeAiLimits.ts +1 -1
  224. package/tui/src/services/compact/autoCompact.ts +1 -1
  225. package/tui/src/services/compact/compact.ts +1 -1
  226. package/tui/src/services/lsp/types.ts +8 -30
  227. package/tui/src/services/tips/types.ts +6 -28
  228. package/tui/src/services/tokenEstimation.ts +1 -1
  229. package/tui/src/services/toolRegistry/bootGuard.ts +5 -5
  230. package/tui/src/services/toolUseSummary/toolUseSummaryGenerator.ts +1 -1
  231. package/tui/src/services/tools/toolExecution.ts +94 -1
  232. package/tui/src/store/pendingPermissionSlot.ts +1 -1
  233. package/tui/src/store/session-store.ts +10 -36
  234. package/tui/src/stubs/any-stub.ts +15 -10
  235. package/tui/src/stubs/color-diff-napi.ts +37 -23
  236. package/tui/src/stubs/globals.d.ts +3 -3
  237. package/tui/src/stubs/macro-preload.ts +23 -12
  238. package/tui/src/tools/AdapterTool/AdapterTool.ts +1207 -714
  239. package/tui/src/tools/AdapterTool/routeDiagnostics.ts +75 -0
  240. package/tui/src/tools/AgentTool/AgentTool.tsx +84 -1371
  241. package/tui/src/tools/AgentTool/agentToolHandoff.ts +114 -0
  242. package/tui/src/tools/AgentTool/agentToolPartialResult.ts +16 -0
  243. package/tui/src/tools/AgentTool/agentToolProgress.ts +32 -0
  244. package/tui/src/tools/AgentTool/agentToolResolver.ts +161 -0
  245. package/tui/src/tools/AgentTool/agentToolResult.ts +163 -0
  246. package/tui/src/tools/AgentTool/agentToolUtils.ts +14 -686
  247. package/tui/src/tools/AgentTool/asyncAgentLifecycle.ts +208 -0
  248. package/tui/src/tools/AgentTool/asyncLifecycle.ts +153 -0
  249. package/tui/src/tools/AgentTool/backgroundedCompletion.ts +126 -0
  250. package/tui/src/tools/AgentTool/backgroundedLifecycle.ts +174 -0
  251. package/tui/src/tools/AgentTool/foregroundBackground.ts +83 -0
  252. package/tui/src/tools/AgentTool/foregroundDrain.tsx +133 -0
  253. package/tui/src/tools/AgentTool/foregroundFinalize.ts +98 -0
  254. package/tui/src/tools/AgentTool/foregroundLifecycle.tsx +237 -0
  255. package/tui/src/tools/AgentTool/foregroundProgress.tsx +169 -0
  256. package/tui/src/tools/AgentTool/foregroundTask.ts +89 -0
  257. package/tui/src/tools/AgentTool/forkSubagent.ts +1 -12
  258. package/tui/src/tools/AgentTool/forkSubagentGate.ts +34 -0
  259. package/tui/src/tools/AgentTool/launchRouting.ts +203 -0
  260. package/tui/src/tools/AgentTool/lifecycle.ts +244 -0
  261. package/tui/src/tools/AgentTool/mcpRouting.ts +73 -0
  262. package/tui/src/tools/AgentTool/orchestrationSupport.ts +70 -0
  263. package/tui/src/tools/AgentTool/permissions.ts +39 -0
  264. package/tui/src/tools/AgentTool/promptSetup.ts +181 -0
  265. package/tui/src/tools/AgentTool/remoteRouting.ts +62 -0
  266. package/tui/src/tools/AgentTool/resultMapping.ts +116 -0
  267. package/tui/src/tools/AgentTool/resumeAgent.ts +39 -107
  268. package/tui/src/tools/AgentTool/resumeAgentHelpers.ts +140 -0
  269. package/tui/src/tools/AgentTool/runAgent.ts +1 -1
  270. package/tui/src/tools/AgentTool/runtimeConfig.ts +57 -0
  271. package/tui/src/tools/AgentTool/schemas.ts +196 -0
  272. package/tui/src/tools/AgentTool/sourceVerificationPropagation.ts +263 -0
  273. package/tui/src/tools/AgentTool/worktreeLifecycle.ts +105 -0
  274. package/tui/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx +174 -202
  275. package/tui/src/tools/BashTool/BashTool.tsx +71 -1072
  276. package/tui/src/tools/BashTool/bashCommandHelpers.ts +12 -12
  277. package/tui/src/tools/BashTool/bashPermissions/astPreflight.ts +173 -0
  278. package/tui/src/tools/BashTool/bashPermissions/classifierChecks.ts +199 -0
  279. package/tui/src/tools/BashTool/bashPermissions/compoundGuards.ts +53 -0
  280. package/tui/src/tools/BashTool/bashPermissions/constants.ts +99 -0
  281. package/tui/src/tools/BashTool/bashPermissions/index.ts +38 -0
  282. package/tui/src/tools/BashTool/bashPermissions/legacyMisparsing.ts +62 -0
  283. package/tui/src/tools/BashTool/bashPermissions/main.ts +135 -0
  284. package/tui/src/tools/BashTool/bashPermissions/normalizedCommands.ts +33 -0
  285. package/tui/src/tools/BashTool/bashPermissions/operatorFlow.ts +98 -0
  286. package/tui/src/tools/BashTool/bashPermissions/permissionChecks.ts +200 -0
  287. package/tui/src/tools/BashTool/bashPermissions/prefixSuggestions.ts +88 -0
  288. package/tui/src/tools/BashTool/bashPermissions/promptClassifierRules.ts +125 -0
  289. package/tui/src/tools/BashTool/bashPermissions/ruleDelegates.ts +19 -0
  290. package/tui/src/tools/BashTool/bashPermissions/ruleMatching.ts +145 -0
  291. package/tui/src/tools/BashTool/bashPermissions/sandboxAutoAllow.ts +75 -0
  292. package/tui/src/tools/BashTool/bashPermissions/subcommandFlow.ts +205 -0
  293. package/tui/src/tools/BashTool/bashPermissions/subcommandGuards.ts +73 -0
  294. package/tui/src/tools/BashTool/bashPermissions/subcommandResultHelpers.ts +116 -0
  295. package/tui/src/tools/BashTool/bashPermissions/types.ts +26 -0
  296. package/tui/src/tools/BashTool/bashPermissions/wrapperStripping.ts +139 -0
  297. package/tui/src/tools/BashTool/bashPermissions.ts +26 -2621
  298. package/tui/src/tools/BashTool/call.ts +202 -0
  299. package/tui/src/tools/BashTool/callLoader.ts +35 -0
  300. package/tui/src/tools/BashTool/commandClassification.ts +151 -0
  301. package/tui/src/tools/BashTool/commandClassificationLoader.ts +40 -0
  302. package/tui/src/tools/BashTool/cwdReset.ts +33 -0
  303. package/tui/src/tools/BashTool/lineTruncation.ts +11 -0
  304. package/tui/src/tools/BashTool/modeValidation.ts +13 -1
  305. package/tui/src/tools/BashTool/outputPersistence.ts +42 -0
  306. package/tui/src/tools/BashTool/permissionClassification.ts +66 -0
  307. package/tui/src/tools/BashTool/permissionLoader.ts +44 -0
  308. package/tui/src/tools/BashTool/resultLoader.ts +29 -0
  309. package/tui/src/tools/BashTool/resultMapping.ts +83 -0
  310. package/tui/src/tools/BashTool/sandboxPolicy.ts +79 -0
  311. package/tui/src/tools/BashTool/schemas.ts +65 -0
  312. package/tui/src/tools/BashTool/sedEditExecution.ts +59 -0
  313. package/tui/src/tools/BashTool/shellExecution.tsx +245 -0
  314. package/tui/src/tools/BashTool/shellOutputUtils.ts +85 -0
  315. package/tui/src/tools/BashTool/shellPermissionGauntlet.ts +97 -0
  316. package/tui/src/tools/BashTool/uiLoader.ts +37 -0
  317. package/tui/src/tools/BriefTool/upload.ts +1 -1
  318. package/tui/src/tools/CalculatorTool/parser.ts +2 -2
  319. package/tui/src/tools/DocumentPrimitive/DocumentPrimitive.ts +262 -0
  320. package/tui/src/tools/DocumentPrimitive/dispatchNormalization.ts +270 -0
  321. package/tui/src/tools/DocumentPrimitive/documentDestinationPath.ts +18 -0
  322. package/tui/src/tools/DocumentPrimitive/documentMutationGuard.ts +22 -0
  323. package/tui/src/tools/DocumentPrimitive/documentPatchNormalization.ts +248 -0
  324. package/tui/src/tools/DocumentPrimitive/documentSourceVerification.ts +245 -0
  325. package/tui/src/tools/DocumentPrimitive/documentSourceVerificationFields.ts +103 -0
  326. package/tui/src/tools/DocumentPrimitive/modelVisibleOutput.ts +40 -0
  327. package/tui/src/tools/DocumentPrimitive/prompt.ts +35 -0
  328. package/tui/src/tools/FileEditTool/FileEditTool.ts +9 -507
  329. package/tui/src/tools/FileEditTool/call.ts +228 -0
  330. package/tui/src/tools/FileEditTool/validateInput.ts +196 -0
  331. package/tui/src/tools/FileReadTool/imageProcessor.ts +13 -0
  332. package/tui/src/tools/FileWriteTool/FileWriteTool.ts +7 -300
  333. package/tui/src/tools/FileWriteTool/call.ts +223 -0
  334. package/tui/src/tools/FileWriteTool/validateInput.ts +80 -0
  335. package/tui/src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts +19 -3
  336. package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +25 -32
  337. package/tui/src/tools/LookupPrimitive/prompt.ts +0 -2
  338. package/tui/src/tools/MCPTool/trustPolicy.ts +118 -0
  339. package/tui/src/tools/McpAuthTool/McpAuthTool.ts +21 -3
  340. package/tui/src/tools/NotebookEditTool/NotebookEditTool.ts +7 -326
  341. package/tui/src/tools/NotebookEditTool/call.ts +254 -0
  342. package/tui/src/tools/NotebookEditTool/notebookModel.ts +51 -0
  343. package/tui/src/tools/NotebookEditTool/validateInput.ts +142 -0
  344. package/tui/src/tools/PowerShellTool/PowerShellTool.tsx +46 -937
  345. package/tui/src/tools/PowerShellTool/acceptEditsCommandValidation.ts +162 -0
  346. package/tui/src/tools/PowerShellTool/call.ts +179 -0
  347. package/tui/src/tools/PowerShellTool/callLoader.ts +37 -0
  348. package/tui/src/tools/PowerShellTool/commandClassification.ts +86 -0
  349. package/tui/src/tools/PowerShellTool/modeValidation.ts +25 -332
  350. package/tui/src/tools/PowerShellTool/outputPersistence.ts +42 -0
  351. package/tui/src/tools/PowerShellTool/permissionClassification.ts +28 -0
  352. package/tui/src/tools/PowerShellTool/resultLoader.ts +31 -0
  353. package/tui/src/tools/PowerShellTool/resultMapping.ts +75 -0
  354. package/tui/src/tools/PowerShellTool/schemas.ts +40 -0
  355. package/tui/src/tools/PowerShellTool/shellExecution.tsx +258 -0
  356. package/tui/src/tools/PowerShellTool/symlinkModeValidation.ts +44 -0
  357. package/tui/src/tools/PowerShellTool/uiLoader.ts +37 -0
  358. package/tui/src/tools/PowerShellTool/validation.ts +39 -0
  359. package/tui/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts +19 -3
  360. package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +1 -11
  361. package/tui/src/tools/ResolveLocationPrimitive/prompt.ts +2 -6
  362. package/tui/src/tools/SkillTool/SkillTool.ts +2 -2
  363. package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +27 -10
  364. package/tui/src/tools/TaskCreateTool/TaskCreateTool.ts +16 -2
  365. package/tui/src/tools/TaskGetTool/TaskGetTool.ts +23 -3
  366. package/tui/src/tools/TaskListTool/TaskListTool.ts +22 -4
  367. package/tui/src/tools/TaskOutputTool/TaskOutputTool.tsx +46 -547
  368. package/tui/src/tools/TaskOutputTool/lookup.ts +216 -0
  369. package/tui/src/tools/TaskOutputTool/render.tsx +257 -0
  370. package/tui/src/tools/TaskOutputTool/schemas.ts +55 -0
  371. package/tui/src/tools/TaskOutputTool/serialization.ts +36 -0
  372. package/tui/src/tools/TaskStopTool/TaskStopTool.ts +10 -0
  373. package/tui/src/tools/TaskUpdateTool/TaskUpdateTool.ts +14 -364
  374. package/tui/src/tools/TaskUpdateTool/completion.ts +62 -0
  375. package/tui/src/tools/TaskUpdateTool/schemas.ts +62 -0
  376. package/tui/src/tools/TaskUpdateTool/serialization.ts +46 -0
  377. package/tui/src/tools/TaskUpdateTool/statusUpdate.ts +247 -0
  378. package/tui/src/tools/TodoWriteTool/TodoWriteTool.ts +21 -2
  379. package/tui/src/tools/ToolSearchTool/ToolSearchTool.ts +21 -302
  380. package/tui/src/tools/ToolSearchTool/ccSupportTools.ts +223 -0
  381. package/tui/src/tools/ToolSearchTool/descriptionCache.ts +50 -0
  382. package/tui/src/tools/ToolSearchTool/keywordSearch.ts +216 -0
  383. package/tui/src/tools/ToolSearchTool/prompt.ts +10 -4
  384. package/tui/src/tools/ToolSearchTool/resultMapping.ts +30 -0
  385. package/tui/src/tools/ToolSearchTool/schemas.ts +30 -0
  386. package/tui/src/tools/ToolSearchTool/searchPool.ts +47 -0
  387. package/tui/src/tools/ToolSearchTool/supportIntentHints.ts +140 -0
  388. package/tui/src/tools/TranslateTool/TranslateTool.ts +1 -1
  389. package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +2 -1
  390. package/tui/src/tools/WebFetchTool/WebFetchTool.ts +43 -138
  391. package/tui/src/tools/WebFetchTool/call.ts +227 -0
  392. package/tui/src/tools/WebFetchTool/resolvedAddressSafety.ts +78 -0
  393. package/tui/src/tools/WebFetchTool/sourceVerification.ts +204 -0
  394. package/tui/src/tools/WebFetchTool/types.ts +23 -0
  395. package/tui/src/tools/WebFetchTool/urlSafety.ts +181 -0
  396. package/tui/src/tools/WebFetchTool/utils.ts +1 -1
  397. package/tui/src/tools/WebSearchTool/UI.tsx +0 -1
  398. package/tui/src/tools/WebSearchTool/WebSearchTool.ts +9 -313
  399. package/tui/src/tools/WebSearchTool/call.ts +33 -0
  400. package/tui/src/tools/WebSearchTool/responseMapping.ts +190 -0
  401. package/tui/src/tools/WebSearchTool/resultBlock.ts +47 -0
  402. package/tui/src/tools/WebSearchTool/schemas.ts +47 -0
  403. package/tui/src/tools/WebSearchTool/toolSchema.ts +12 -0
  404. package/tui/src/tools/WorkspaceToolAdapter/WorkspaceToolAdapter.ts +79 -0
  405. package/tui/src/tools/WorkspaceToolAdapter/allowedRootPolicy.ts +85 -0
  406. package/tui/src/tools/WorkspaceToolAdapter/documentFormatGuards.ts +73 -0
  407. package/tui/src/tools/WorkspaceToolAdapter/inputNormalization.ts +105 -0
  408. package/tui/src/tools/WorkspaceToolAdapter/mcpExposurePolicy.ts +64 -0
  409. package/tui/src/tools/WorkspaceToolAdapter/toolDefFactory.ts +215 -0
  410. package/tui/src/tools/WorkspaceToolAdapter/toolNames.ts +6 -0
  411. package/tui/src/tools/WorkspaceToolAdapter/workspacePolicy.ts +15 -0
  412. package/tui/src/tools/_shared/dispatchPrimitive.ts +6 -6
  413. package/tui/src/tools/_shared/documentChangeToPatch.ts +125 -0
  414. package/tui/src/tools/_shared/documentDispatchArguments.ts +87 -0
  415. package/tui/src/tools/_shared/documentPrimitiveTimeout.ts +13 -0
  416. package/tui/src/tools/_shared/documentToolResultRender.ts +98 -0
  417. package/tui/src/tools/_shared/pendingCallRegistry.ts +1 -6
  418. package/tui/src/tools/_shared/rootPrimitiveInput.ts +1 -0
  419. package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPatterns.ts +58 -0
  420. package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPrompt.ts +271 -0
  421. package/tui/src/tools/_shared/toolChoiceRepair/documentRepair.ts +452 -0
  422. package/tui/src/tools/_shared/toolChoiceRepair/messageAccess.ts +80 -0
  423. package/tui/src/tools/_shared/toolChoiceRepair/publicDataRepair.ts +92 -0
  424. package/tui/src/tools/_shared/toolChoiceRepair/supportRepair.ts +135 -0
  425. package/tui/src/tools/_shared/toolChoiceRepair.ts +55 -860
  426. package/tui/src/tools/shared/mockDisclaimer.ts +1 -1
  427. package/tui/src/tools.ts +39 -190
  428. package/tui/src/types/fileSuggestion.ts +4 -26
  429. package/tui/src/types/generated/events_mono/claude_code/v1/claude_code_internal_event.ts +186 -148
  430. package/tui/src/types/generated/events_mono/common/v1/auth.ts +25 -11
  431. package/tui/src/types/generated/events_mono/growthbook/v1/growthbook_experiment_event.ts +47 -30
  432. package/tui/src/types/generated/google/protobuf/timestamp.ts +21 -7
  433. package/tui/src/types/message.ts +80 -102
  434. package/tui/src/types/messageQueueTypes.ts +6 -28
  435. package/tui/src/types/notebook.ts +16 -38
  436. package/tui/src/types/statusLine.ts +4 -26
  437. package/tui/src/types/tools.ts +24 -46
  438. package/tui/src/types/utils.ts +6 -28
  439. package/tui/src/upstreamproxy/relay.ts +7 -3
  440. package/tui/src/upstreamproxy/upstreamproxy.ts +1 -1
  441. package/tui/src/utils/assistantMessageFactories.ts +9 -3
  442. package/tui/src/utils/auth.ts +129 -139
  443. package/tui/src/utils/bash/ast.ts +23 -23
  444. package/tui/src/utils/bash/bashParser.ts +5 -5
  445. package/tui/src/utils/billing.ts +1 -1
  446. package/tui/src/utils/claudeDesktop.ts +4 -4
  447. package/tui/src/utils/collapseReadSearch.ts +3 -3
  448. package/tui/src/utils/cronTasks.ts +1 -1
  449. package/tui/src/utils/execFileNoThrow.ts +1 -1
  450. package/tui/src/utils/filePersistence/types.ts +16 -38
  451. package/tui/src/utils/forkedAgent.ts +1 -1
  452. package/tui/src/utils/gracefulShutdown.ts +4 -4
  453. package/tui/src/utils/heapDumpService.ts +12 -8
  454. package/tui/src/utils/hooks/apiQueryHookHelper.ts +1 -1
  455. package/tui/src/utils/hooks/execPromptHook.ts +1 -1
  456. package/tui/src/utils/hooks/skillImprovement.ts +1 -1
  457. package/tui/src/utils/mcp/dateTimeParser.ts +1 -1
  458. package/tui/src/utils/messages.ts +18 -0
  459. package/tui/src/utils/migrateSessions.ts +3 -3
  460. package/tui/src/utils/model/model.ts +6 -6
  461. package/tui/src/utils/permissions/yoloClassifier.ts +1 -1
  462. package/tui/src/utils/plugins/headlessPluginInstall.ts +1 -1
  463. package/tui/src/utils/plugins/mcpPluginIntegration.ts +1 -1
  464. package/tui/src/utils/plugins/mcpbHandler.ts +1 -1
  465. package/tui/src/utils/plugins/pluginLoader.ts +8 -8
  466. package/tui/src/utils/protectedNamespace.ts +5 -3
  467. package/tui/src/utils/rawJsonToolCall.ts +242 -0
  468. package/tui/src/utils/ripgrep.ts +16 -7
  469. package/tui/src/utils/sessionTitle.ts +1 -1
  470. package/tui/src/utils/settings/permissionValidation.ts +14 -2
  471. package/tui/src/utils/shell/prefix.ts +1 -1
  472. package/tui/src/utils/sideQuery.ts +1 -1
  473. package/tui/src/utils/systemThemeWatcher.ts +13 -3
  474. package/tui/src/utils/teleport.tsx +1 -1
  475. package/uv.lock +426 -45
  476. package/tui/src/services/api/claude.ts +0 -3540
  477. package/tui/src/tools/_shared/directPublicDataGuard.ts +0 -362
  478. package/tui/src/tools/_shared/kmaAnalysisGuard.ts +0 -197
  479. package/tui/src/tools/_shared/kmaAviationGuard.ts +0 -70
  480. package/tui/src/tools/_shared/nmcAedGuard.ts +0 -234
  481. package/tui/src/tools/_shared/protectedCheckGuard.ts +0 -207
  482. package/tui/src/tools/_shared/textToolCallGuard.ts +0 -91
@@ -0,0 +1,1030 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """OOXML adapter and engine boundaries for DOCX, XLSX, and PPTX."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import html
7
+ import re
8
+ from copy import copy
9
+ from datetime import date, datetime
10
+ from decimal import Decimal
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, BinaryIO, Protocol, cast
13
+
14
+ import docx
15
+ import openpyxl # type: ignore[import-untyped]
16
+ import pptx
17
+ from docx.document import Document as DocxDocument
18
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
19
+ from docx.oxml import OxmlElement
20
+ from docx.oxml.ns import qn
21
+ from docx.shared import Pt, RGBColor
22
+ from docx.table import Table as DocxTable
23
+ from docx.table import _Cell as DocxCell
24
+ from docx.text.paragraph import Paragraph as DocxParagraph
25
+ from docx.text.run import Run as DocxRun
26
+ from openpyxl.cell.cell import Cell # type: ignore[import-untyped]
27
+ from openpyxl.styles import Alignment, Font, PatternFill # type: ignore[import-untyped]
28
+ from openpyxl.worksheet.worksheet import Worksheet # type: ignore[import-untyped]
29
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
30
+ from pptx.presentation import Presentation as PptxPresentation
31
+ from pptx.slide import Slide as PptxSlide
32
+ from pptx.table import Table as PptxTable
33
+
34
+ from ummaya.tools.documents.engines import DocumentInspectionEngine, DocumentMutationEngine
35
+ from ummaya.tools.documents.models import (
36
+ DocumentExtraction,
37
+ DocumentFormat,
38
+ DocumentPatch,
39
+ DocumentPatchOperation,
40
+ ImageReference,
41
+ KnownDocumentFormat,
42
+ MetadataValue,
43
+ OperationType,
44
+ ParagraphBlock,
45
+ ScalarValue,
46
+ StyleDescriptor,
47
+ TableBlock,
48
+ TableCell,
49
+ )
50
+
51
+ if TYPE_CHECKING:
52
+ from ummaya.tools.documents.tool_defs import DocumentFieldPatch
53
+
54
+
55
+ class _OfficeSaveable(Protocol):
56
+ """Document object that can save itself to a binary file-like stream."""
57
+
58
+ def save(self, target: BinaryIO) -> None:
59
+ """Persist the office document into the provided binary stream."""
60
+
61
+
62
+ OOXML_CANDIDATE_ENGINES: dict[DocumentFormat, tuple[str, ...]] = {
63
+ DocumentFormat.docx: ("python-docx", "direct-wordprocessingml-oracle"),
64
+ DocumentFormat.xlsx: ("openpyxl", "direct-spreadsheetml-oracle"),
65
+ DocumentFormat.pptx: ("python-pptx", "direct-presentationml-oracle"),
66
+ }
67
+
68
+ _DOCX_PARAGRAPH_RE = re.compile(r"(?:^|/)paragraphs?/(?P<paragraph>\d+)(?:/runs/(?P<run>\d+))?$")
69
+ _DOCX_TABLE_CELL_RE = re.compile(
70
+ r"(?:^|/)tables?/(?P<table>\d+)/rows?/(?P<row>\d+)/cells?/(?P<cell>\d+)$|"
71
+ r"(?:^|/)table/(?P<table2>\d+)/r(?P<row2>\d+)c(?P<cell2>\d+)$"
72
+ )
73
+ _XLSX_CELL_RE = re.compile(r"^/sheets/(?P<sheet>[^/]+)/cells/(?P<cell>[A-Za-z]{1,3}\d+)$")
74
+ _PPTX_SHAPE_TEXT_RE = re.compile(r"^/slides/(?P<slide>\d+)/shapes/(?P<shape>\d+)/text$")
75
+ _PPTX_TABLE_CELL_RE = re.compile(
76
+ r"^/slides/(?P<slide>\d+)/tables/(?P<table>\d+)/rows/(?P<row>\d+)/cells/(?P<cell>\d+)$"
77
+ )
78
+
79
+
80
+ def validate_ooxml_engine(engine: DocumentInspectionEngine) -> DocumentInspectionEngine:
81
+ """Validate that an injected engine is scoped to an OOXML format."""
82
+ if engine.document_format not in OOXML_CANDIDATE_ENGINES:
83
+ raise ValueError("OOXML adapter requires a docx, xlsx, or pptx engine")
84
+ return engine
85
+
86
+
87
+ def validate_ooxml_mutation_engine(engine: DocumentInspectionEngine) -> DocumentMutationEngine:
88
+ """Validate that an injected OOXML engine can safely mutate derivatives."""
89
+ validate_ooxml_engine(engine)
90
+ if not isinstance(engine, DocumentMutationEngine):
91
+ raise ValueError("OOXML adapter requires a mutation-capable engine")
92
+ return engine
93
+
94
+
95
+ class _OoxmlAdapterBase:
96
+ """Shared adapter behavior for one OOXML file family."""
97
+
98
+ adapter_id: str
99
+ known_formats: tuple[KnownDocumentFormat, ...]
100
+ promoted_formats: tuple[DocumentFormat, ...]
101
+
102
+ def __init__(self, inspection_engine: DocumentInspectionEngine | None = None) -> None:
103
+ self._inspection_engine = (
104
+ validate_ooxml_engine(inspection_engine) if inspection_engine is not None else None
105
+ )
106
+
107
+ @property
108
+ def engine_id(self) -> str:
109
+ """Return the wrapped engine id for diagnostics."""
110
+ if self._inspection_engine is None:
111
+ return self.adapter_id
112
+ return self._inspection_engine.engine_id
113
+
114
+ def inspect(self, path: Path, *, artifact_id: str) -> DocumentExtraction:
115
+ """Delegate inspection when promoted; otherwise return a typed empty scope."""
116
+ if self._inspection_engine is None:
117
+ return DocumentExtraction(
118
+ artifact_id=artifact_id,
119
+ metadata={"adapter_id": self.adapter_id},
120
+ warnings=[f"{self.adapter_id} is registered as known-only."],
121
+ )
122
+ return self._inspection_engine.inspect(path, artifact_id=artifact_id)
123
+
124
+ def normalize_fill_patches(
125
+ self,
126
+ patches: tuple[DocumentFieldPatch, ...],
127
+ *,
128
+ extraction: DocumentExtraction | None,
129
+ ) -> tuple[DocumentFieldPatch, ...]:
130
+ """Return fill patches unchanged for OOXML adapters."""
131
+ _ = extraction
132
+ return patches
133
+
134
+
135
+ class DocxDocumentAdapter(_OoxmlAdapterBase):
136
+ """DOCX adapter boundary backed by python-docx."""
137
+
138
+ adapter_id: str = "python-docx-adapter"
139
+ known_formats: tuple[KnownDocumentFormat, ...] = (KnownDocumentFormat.docx,)
140
+ promoted_formats: tuple[DocumentFormat, ...] = (DocumentFormat.docx,)
141
+
142
+ def __init__(self, inspection_engine: DocumentInspectionEngine | None = None) -> None:
143
+ super().__init__(inspection_engine or PythonDocxDocumentEngine())
144
+
145
+
146
+ class XlsxDocumentAdapter(_OoxmlAdapterBase):
147
+ """XLSX adapter boundary backed by openpyxl when promoted."""
148
+
149
+ adapter_id: str = "openpyxl-adapter"
150
+ known_formats: tuple[KnownDocumentFormat, ...] = (KnownDocumentFormat.xlsx,)
151
+ promoted_formats: tuple[DocumentFormat, ...] = ()
152
+
153
+ def __init__(self, inspection_engine: DocumentInspectionEngine | None = None) -> None:
154
+ self.promoted_formats = (DocumentFormat.xlsx,) if inspection_engine is not None else ()
155
+ super().__init__(inspection_engine)
156
+
157
+
158
+ class PptxDocumentAdapter(_OoxmlAdapterBase):
159
+ """PPTX adapter boundary backed by python-pptx when promoted."""
160
+
161
+ adapter_id: str = "python-pptx-adapter"
162
+ known_formats: tuple[KnownDocumentFormat, ...] = (KnownDocumentFormat.pptx,)
163
+ promoted_formats: tuple[DocumentFormat, ...] = ()
164
+
165
+ def __init__(self, inspection_engine: DocumentInspectionEngine | None = None) -> None:
166
+ self.promoted_formats = (DocumentFormat.pptx,) if inspection_engine is not None else ()
167
+ super().__init__(inspection_engine)
168
+
169
+
170
+ class PythonDocxDocumentEngine:
171
+ """DOCX read/write engine backed by the promoted python-docx dependency."""
172
+
173
+ document_format = DocumentFormat.docx
174
+ engine_id = "python-docx"
175
+ render_artifact_extension = "svg"
176
+ render_mime_type = "image/svg+xml"
177
+
178
+ def inspect(self, path: Path, *, artifact_id: str) -> DocumentExtraction:
179
+ """Extract normalized paragraphs, tables, and core metadata from DOCX."""
180
+ document = docx.Document(str(path))
181
+ paragraphs: list[ParagraphBlock] = []
182
+ tables: list[TableBlock] = []
183
+
184
+ paragraph_index = 1
185
+ table_index = 1
186
+ for block in document.iter_inner_content():
187
+ if isinstance(block, DocxParagraph):
188
+ if block.text:
189
+ paragraphs.append(
190
+ _paragraph_block(
191
+ block,
192
+ engine_id=self.engine_id,
193
+ path=path,
194
+ index=paragraph_index,
195
+ )
196
+ )
197
+ paragraph_index += 1
198
+ elif isinstance(block, DocxTable):
199
+ tables.append(
200
+ _table_block(
201
+ block,
202
+ engine_id=self.engine_id,
203
+ path=path,
204
+ index=table_index,
205
+ )
206
+ )
207
+ table_index += 1
208
+
209
+ return DocumentExtraction(
210
+ artifact_id=artifact_id,
211
+ paragraphs=paragraphs,
212
+ tables=tables,
213
+ metadata=_docx_core_metadata(document),
214
+ warnings=[
215
+ "python-docx scope excludes nested tables and tracked revision "
216
+ "content from the top-level document lists."
217
+ ],
218
+ )
219
+
220
+ def apply_patch(self, path: Path, patch: DocumentPatch) -> bytes:
221
+ """Apply bounded paragraph, run, table-cell, style, and metadata edits."""
222
+ document = docx.Document(str(path))
223
+ for operation in patch.operations:
224
+ _apply_docx_operation(document, operation)
225
+ output = _save_to_bytes(document)
226
+ return output
227
+
228
+ def render(self, path: Path, *, artifact_id: str, output_dir: Path) -> tuple[bytes, ...]:
229
+ """Render a lightweight SVG evidence page for DOCX review."""
230
+ _ = output_dir
231
+ extraction = self.inspect(path, artifact_id=artifact_id)
232
+ lines = [block.text for block in extraction.paragraphs]
233
+ lines.extend(cell.text for table in extraction.tables for cell in table.cells if cell.text)
234
+ return (_svg_page(lines or [Path(path).name], title=f"DOCX {artifact_id}"),)
235
+
236
+
237
+ PythonDocxInspectionEngine = PythonDocxDocumentEngine
238
+
239
+
240
+ class OpenPyxlDocumentEngine:
241
+ """XLSX read/write engine backed by openpyxl."""
242
+
243
+ document_format = DocumentFormat.xlsx
244
+ engine_id = "openpyxl"
245
+ render_artifact_extension = "svg"
246
+ render_mime_type = "image/svg+xml"
247
+
248
+ def inspect(self, path: Path, *, artifact_id: str) -> DocumentExtraction:
249
+ """Extract normalized sheet cells, metadata, and style anchors."""
250
+ workbook = openpyxl.load_workbook(path, data_only=False)
251
+ tables: list[TableBlock] = []
252
+ style_map: list[StyleDescriptor] = []
253
+ metadata: dict[str, MetadataValue] = {
254
+ "engine_id": self.engine_id,
255
+ "format": "xlsx",
256
+ "sheet_count": len(workbook.worksheets),
257
+ }
258
+ for sheet_index, worksheet in enumerate(workbook.worksheets, start=1):
259
+ cells: list[TableCell] = []
260
+ for row in worksheet.iter_rows():
261
+ for cell in row:
262
+ if cell.value is None:
263
+ continue
264
+ source_path = f"/sheets/{worksheet.title}/cells/{cell.coordinate}"
265
+ cells.append(
266
+ TableCell(
267
+ row_index=cell.row - 1,
268
+ column_index=cell.column - 1,
269
+ text=str(cell.value),
270
+ source_path=source_path,
271
+ field_path=source_path,
272
+ )
273
+ )
274
+ if _cell_has_non_default_style(cell):
275
+ style_map.append(_xlsx_style_descriptor(cell, source_path))
276
+ tables.append(
277
+ TableBlock(
278
+ block_id=f"xlsx-sheet-{sheet_index:03d}",
279
+ source_path=f"/sheets/{worksheet.title}",
280
+ cells=cells,
281
+ )
282
+ )
283
+ if worksheet.print_area:
284
+ metadata[f"sheet_{sheet_index}_print_area"] = str(worksheet.print_area)
285
+
286
+ return DocumentExtraction(
287
+ artifact_id=artifact_id,
288
+ tables=tables,
289
+ metadata=metadata,
290
+ style_map=style_map,
291
+ warnings=[
292
+ "openpyxl preserves formula strings but UMMAYA does not claim formula "
293
+ "evaluation or cached-value recalculation."
294
+ ],
295
+ )
296
+
297
+ def apply_patch(self, path: Path, patch: DocumentPatch) -> bytes:
298
+ """Apply bounded cell, cell-style, and workbook metadata edits."""
299
+ workbook = openpyxl.load_workbook(path, data_only=False)
300
+ for operation in patch.operations:
301
+ _apply_xlsx_operation(workbook, operation)
302
+ return _save_workbook_to_bytes(workbook)
303
+
304
+ def render(self, path: Path, *, artifact_id: str, output_dir: Path) -> tuple[bytes, ...]:
305
+ """Render one SVG evidence page per worksheet."""
306
+ _ = output_dir
307
+ extraction = self.inspect(path, artifact_id=artifact_id)
308
+ pages: list[bytes] = []
309
+ for table in extraction.tables:
310
+ lines = [cell.text for cell in table.cells[:36]]
311
+ pages.append(_svg_page(lines or [table.source_path], title=table.block_id))
312
+ return tuple(pages) or (_svg_page([Path(path).name], title=f"XLSX {artifact_id}"),)
313
+
314
+
315
+ class PythonPptxDocumentEngine:
316
+ """PPTX read/write engine backed by python-pptx."""
317
+
318
+ document_format = DocumentFormat.pptx
319
+ engine_id = "python-pptx"
320
+ render_artifact_extension = "svg"
321
+ render_mime_type = "image/svg+xml"
322
+
323
+ def inspect(self, path: Path, *, artifact_id: str) -> DocumentExtraction:
324
+ """Extract normalized slide text, tables, images, and core metadata."""
325
+ presentation = pptx.Presentation(str(path))
326
+ paragraphs: list[ParagraphBlock] = []
327
+ tables: list[TableBlock] = []
328
+ images: list[ImageReference] = []
329
+ for slide_index, slide in enumerate(presentation.slides, start=1):
330
+ table_index = 1
331
+ for shape_index, shape in enumerate(slide.shapes, start=1):
332
+ if getattr(shape, "has_text_frame", False) and shape.text:
333
+ paragraphs.append(
334
+ ParagraphBlock(
335
+ block_id=f"pptx-slide-{slide_index:03d}-shape-{shape_index:03d}",
336
+ text=shape.text,
337
+ source_path=f"/slides/{slide_index}/shapes/{shape_index}/text",
338
+ )
339
+ )
340
+ if getattr(shape, "has_table", False):
341
+ tables.append(
342
+ _pptx_table_block(
343
+ cast(PptxTable, shape.table),
344
+ slide_index=slide_index,
345
+ table_index=table_index,
346
+ )
347
+ )
348
+ table_index += 1
349
+ if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
350
+ images.append(
351
+ ImageReference(
352
+ image_id=f"pptx-slide-{slide_index:03d}-image-{shape_index:03d}",
353
+ source_path=f"/slides/{slide_index}/images/{shape_index}",
354
+ content_type=getattr(shape.image, "content_type", "image/unknown"),
355
+ alt_text=getattr(shape, "name", None),
356
+ )
357
+ )
358
+
359
+ return DocumentExtraction(
360
+ artifact_id=artifact_id,
361
+ paragraphs=paragraphs,
362
+ tables=tables,
363
+ images=images,
364
+ metadata=_pptx_core_metadata(presentation),
365
+ warnings=[
366
+ "python-pptx scope blocks animations, masters, and complex media rewrites "
367
+ "until separate promotion gates pass."
368
+ ],
369
+ )
370
+
371
+ def apply_patch(self, path: Path, patch: DocumentPatch) -> bytes:
372
+ """Apply bounded slide text, table-cell, and core metadata edits."""
373
+ presentation = pptx.Presentation(str(path))
374
+ for operation in patch.operations:
375
+ _apply_pptx_operation(presentation, operation)
376
+ return _save_to_bytes(presentation)
377
+
378
+ def render(self, path: Path, *, artifact_id: str, output_dir: Path) -> tuple[bytes, ...]:
379
+ """Render one SVG evidence page per slide."""
380
+ _ = output_dir
381
+ extraction = self.inspect(path, artifact_id=artifact_id)
382
+ by_slide: dict[int, list[str]] = {}
383
+ for block in extraction.paragraphs:
384
+ slide_index = _slide_index_from_path(block.source_path)
385
+ by_slide.setdefault(slide_index, []).append(block.text)
386
+ for table in extraction.tables:
387
+ slide_index = _slide_index_from_path(table.source_path)
388
+ by_slide.setdefault(slide_index, []).extend(cell.text for cell in table.cells)
389
+ return tuple(
390
+ _svg_page(lines or [f"Slide {slide_index}"], title=f"PPTX slide {slide_index}")
391
+ for slide_index, lines in sorted(by_slide.items())
392
+ ) or (_svg_page([Path(path).name], title=f"PPTX {artifact_id}"),)
393
+
394
+
395
+ def _paragraph_block(
396
+ paragraph: DocxParagraph,
397
+ *,
398
+ engine_id: str,
399
+ path: Path,
400
+ index: int,
401
+ ) -> ParagraphBlock:
402
+ return ParagraphBlock(
403
+ block_id=f"docx-paragraph-{index:03d}",
404
+ text=paragraph.text,
405
+ source_path=f"engine://{engine_id}/{path.name}/paragraph/{index}",
406
+ style_id=_paragraph_style_id(paragraph),
407
+ )
408
+
409
+
410
+ def _table_block(
411
+ table: DocxTable,
412
+ *,
413
+ engine_id: str,
414
+ path: Path,
415
+ index: int,
416
+ ) -> TableBlock:
417
+ cells: list[TableCell] = []
418
+ for row_index, row in enumerate(table.rows):
419
+ row_cells = tuple(row.cells)
420
+ for column_index, cell in enumerate(row_cells):
421
+ source_path = (
422
+ f"engine://{engine_id}/{path.name}/table/{index}/"
423
+ f"r{row_index + 1}c{column_index + 1}"
424
+ )
425
+ cells.append(
426
+ TableCell(
427
+ row_index=row_index,
428
+ column_index=column_index,
429
+ text=cell.text,
430
+ source_path=source_path,
431
+ field_path=(
432
+ source_path
433
+ if _docx_adjacent_label_blank_value_cell(row_cells, column_index)
434
+ else None
435
+ ),
436
+ )
437
+ )
438
+ return TableBlock(
439
+ block_id=f"docx-table-{index:03d}",
440
+ source_path=f"engine://{engine_id}/{path.name}/table/{index}",
441
+ cells=cells,
442
+ )
443
+
444
+
445
+ def _docx_adjacent_label_blank_value_cell(
446
+ row_cells: tuple[DocxCell, ...],
447
+ column_index: int,
448
+ ) -> bool:
449
+ if column_index <= 0:
450
+ return False
451
+ if row_cells[column_index].text.strip():
452
+ return False
453
+ return _docx_meaningful_form_label(row_cells[column_index - 1].text)
454
+
455
+
456
+ def _docx_meaningful_form_label(text: str) -> bool:
457
+ normalized = re.sub(r"\s+", "", text)
458
+ if len(normalized) < 2:
459
+ return False
460
+ return re.search(r"[0-9A-Za-z가-힣]", normalized) is not None
461
+
462
+
463
+ def _paragraph_style_id(paragraph: DocxParagraph) -> str | None:
464
+ style: object | None = paragraph.style
465
+ style_id = getattr(style, "style_id", None)
466
+ if isinstance(style_id, str) and style_id:
467
+ return style_id
468
+ style_name = getattr(style, "name", None)
469
+ if isinstance(style_name, str) and style_name:
470
+ return style_name
471
+ return None
472
+
473
+
474
+ def _docx_core_metadata(document: DocxDocument) -> dict[str, MetadataValue]:
475
+ core_properties = document.core_properties
476
+ metadata: dict[str, MetadataValue] = {
477
+ "engine_id": "python-docx",
478
+ "format": "docx",
479
+ }
480
+ for property_name in (
481
+ "author",
482
+ "category",
483
+ "comments",
484
+ "content_status",
485
+ "created",
486
+ "identifier",
487
+ "keywords",
488
+ "language",
489
+ "last_modified_by",
490
+ "last_printed",
491
+ "modified",
492
+ "revision",
493
+ "subject",
494
+ "title",
495
+ "version",
496
+ ):
497
+ value = getattr(core_properties, property_name)
498
+ if _metadata_value_is_present(value):
499
+ metadata[f"core_{property_name}"] = value
500
+ return metadata
501
+
502
+
503
+ def _metadata_value_is_present(value: MetadataValue) -> bool:
504
+ if isinstance(value, str):
505
+ return bool(value)
506
+ if isinstance(value, datetime):
507
+ return True
508
+ return value is not None
509
+
510
+
511
+ def _apply_docx_operation(document: DocxDocument, operation: DocumentPatchOperation) -> None:
512
+ if operation.operation_type in {
513
+ OperationType.replace_text,
514
+ OperationType.set_field_value,
515
+ OperationType.insert_paragraph,
516
+ }:
517
+ _apply_docx_text_operation(document, operation)
518
+ return
519
+ if operation.operation_type is OperationType.set_table_cell:
520
+ table, row_index, cell_index = _docx_table_cell(document, operation.target_path)
521
+ _ = table
522
+ cell = document.tables[_docx_table_ordinal(operation.target_path) - 1].cell(
523
+ row_index,
524
+ cell_index,
525
+ )
526
+ _set_docx_paragraph_text(cell.paragraphs[0], _string_value(operation.value))
527
+ return
528
+ if operation.operation_type is OperationType.set_document_metadata:
529
+ _set_docx_metadata(document, operation)
530
+ return
531
+ if operation.operation_type is OperationType.set_run_style:
532
+ paragraph_index, run_index = _docx_paragraph_and_run_indexes(operation.target_path)
533
+ _apply_docx_run_style(document.paragraphs[paragraph_index], run_index, operation.style)
534
+ return
535
+ if operation.operation_type is OperationType.set_paragraph_style:
536
+ paragraph_index, _ = _docx_paragraph_and_run_indexes(operation.target_path)
537
+ _apply_docx_paragraph_style(document.paragraphs[paragraph_index], operation.style)
538
+ return
539
+ if operation.operation_type is OperationType.set_cell_style:
540
+ table, row_index, cell_index = _docx_table_cell(document, operation.target_path)
541
+ cell = table.cell(row_index, cell_index)
542
+ _apply_docx_cell_style(cell, operation.style)
543
+ return
544
+ raise ValueError(f"Unsupported DOCX operation: {operation.operation_type.value}")
545
+
546
+
547
+ def _apply_docx_text_operation(
548
+ document: DocxDocument,
549
+ operation: DocumentPatchOperation,
550
+ ) -> None:
551
+ paragraph_index, run_index = _docx_paragraph_and_run_indexes(operation.target_path)
552
+ paragraph = document.paragraphs[paragraph_index]
553
+ if operation.operation_type is OperationType.insert_paragraph:
554
+ document.add_paragraph(_string_value(operation.value))
555
+ return
556
+ if run_index is None:
557
+ _set_docx_paragraph_text(paragraph, _string_value(operation.value))
558
+ return
559
+ while len(paragraph.runs) <= run_index:
560
+ paragraph.add_run("")
561
+ paragraph.runs[run_index].text = _string_value(operation.value)
562
+
563
+
564
+ def _set_docx_paragraph_text(paragraph: DocxParagraph, value: str) -> None:
565
+ if paragraph.runs:
566
+ paragraph.runs[0].text = value
567
+ for run in paragraph.runs[1:]:
568
+ run.text = ""
569
+ else:
570
+ paragraph.add_run(value)
571
+
572
+
573
+ def _docx_paragraph_and_run_indexes(target_path: str) -> tuple[int, int | None]:
574
+ match = _DOCX_PARAGRAPH_RE.search(target_path)
575
+ if match is None:
576
+ raise ValueError(f"Unsupported DOCX paragraph target: {target_path}")
577
+ paragraph_index = int(match.group("paragraph")) - 1
578
+ run_value = match.group("run")
579
+ return paragraph_index, int(run_value) - 1 if run_value is not None else None
580
+
581
+
582
+ def _docx_table_ordinal(target_path: str) -> int:
583
+ match = _DOCX_TABLE_CELL_RE.search(target_path)
584
+ if match is None:
585
+ raise ValueError(f"Unsupported DOCX table target: {target_path}")
586
+ return int(match.group("table") or match.group("table2"))
587
+
588
+
589
+ def _docx_table_cell(
590
+ document: DocxDocument,
591
+ target_path: str,
592
+ ) -> tuple[DocxTable, int, int]:
593
+ match = _DOCX_TABLE_CELL_RE.search(target_path)
594
+ if match is None:
595
+ raise ValueError(f"Unsupported DOCX table target: {target_path}")
596
+ table_index = int(match.group("table") or match.group("table2")) - 1
597
+ row_index = int(match.group("row") or match.group("row2")) - 1
598
+ cell_index = int(match.group("cell") or match.group("cell2")) - 1
599
+ return document.tables[table_index], row_index, cell_index
600
+
601
+
602
+ def _set_docx_metadata(document: DocxDocument, operation: DocumentPatchOperation) -> None:
603
+ property_name = operation.target_path.rsplit("/", 1)[-1]
604
+ if property_name not in {
605
+ "author",
606
+ "category",
607
+ "comments",
608
+ "content_status",
609
+ "identifier",
610
+ "keywords",
611
+ "language",
612
+ "last_modified_by",
613
+ "revision",
614
+ "subject",
615
+ "title",
616
+ "version",
617
+ }:
618
+ raise ValueError(f"Unsupported DOCX core metadata target: {operation.target_path}")
619
+ setattr(document.core_properties, property_name, _string_value(operation.value))
620
+
621
+
622
+ def _apply_docx_run_style(
623
+ paragraph: DocxParagraph,
624
+ run_index: int | None,
625
+ style: StyleDescriptor | None,
626
+ ) -> None:
627
+ if run_index is None:
628
+ raise ValueError("DOCX run style target must include /runs/{index}")
629
+ if style is None:
630
+ raise ValueError("DOCX run style operation requires style")
631
+ while len(paragraph.runs) <= run_index:
632
+ paragraph.add_run("")
633
+ run = paragraph.runs[run_index]
634
+ if style.bold is not None:
635
+ run.bold = style.bold
636
+ if style.italic is not None:
637
+ run.italic = style.italic
638
+ if style.underline is not None:
639
+ run.underline = style.underline
640
+ if style.font_family is not None:
641
+ _set_docx_run_font_family(run, style.font_family)
642
+ if style.font_size_pt is not None:
643
+ run.font.size = Pt(float(style.font_size_pt))
644
+ if style.font_color_rgb is not None:
645
+ run.font.color.rgb = RGBColor.from_string(style.font_color_rgb)
646
+
647
+
648
+ def _apply_docx_paragraph_style(
649
+ paragraph: DocxParagraph,
650
+ style: StyleDescriptor | None,
651
+ ) -> None:
652
+ if style is None:
653
+ raise ValueError("DOCX paragraph style operation requires style")
654
+ _apply_docx_paragraph_alignment(paragraph, style)
655
+ if _docx_style_has_direct_run_properties(style):
656
+ _apply_docx_direct_style_to_paragraph_runs(paragraph, style)
657
+ return
658
+ try:
659
+ paragraph.style = style.style_id
660
+ except KeyError:
661
+ return
662
+
663
+
664
+ def _apply_docx_cell_style(cell: DocxCell, style: StyleDescriptor | None) -> None:
665
+ if style is None:
666
+ raise ValueError("DOCX cell style operation requires style")
667
+ if style.fill_color_rgb is not None:
668
+ _apply_docx_cell_fill(cell, style.fill_color_rgb)
669
+ for paragraph in cell.paragraphs:
670
+ _apply_docx_paragraph_alignment(paragraph, style)
671
+ if _docx_style_has_direct_run_properties(style):
672
+ _apply_docx_direct_style_to_paragraph_runs(paragraph, style)
673
+
674
+
675
+ def _apply_docx_cell_fill(cell: DocxCell, fill_color_rgb: str) -> None:
676
+ tc_pr = cell._tc.get_or_add_tcPr() # noqa: SLF001 - python-docx exposes cell shading only via OOXML.
677
+ shading = tc_pr.find(qn("w:shd"))
678
+ if shading is None:
679
+ shading = OxmlElement("w:shd")
680
+ tc_pr.append(shading)
681
+ shading.set(qn("w:fill"), fill_color_rgb.upper())
682
+
683
+
684
+ def _apply_docx_paragraph_alignment(
685
+ paragraph: DocxParagraph,
686
+ style: StyleDescriptor,
687
+ ) -> None:
688
+ if style.alignment is None:
689
+ return
690
+ alignment = {
691
+ "left": WD_ALIGN_PARAGRAPH.LEFT,
692
+ "center": WD_ALIGN_PARAGRAPH.CENTER,
693
+ "right": WD_ALIGN_PARAGRAPH.RIGHT,
694
+ "justify": WD_ALIGN_PARAGRAPH.JUSTIFY,
695
+ "distributed": WD_ALIGN_PARAGRAPH.DISTRIBUTE,
696
+ }[style.alignment]
697
+ paragraph.alignment = alignment
698
+
699
+
700
+ def _docx_style_has_direct_run_properties(style: StyleDescriptor) -> bool:
701
+ return any(
702
+ value is not None
703
+ for value in (
704
+ style.bold,
705
+ style.italic,
706
+ style.underline,
707
+ style.font_family,
708
+ style.font_size_pt,
709
+ style.font_color_rgb,
710
+ )
711
+ )
712
+
713
+
714
+ def _apply_docx_direct_style_to_paragraph_runs(
715
+ paragraph: DocxParagraph,
716
+ style: StyleDescriptor,
717
+ ) -> None:
718
+ runs = paragraph.runs or [paragraph.add_run("")]
719
+ for run in runs:
720
+ _apply_docx_direct_run_style(run, style)
721
+
722
+
723
+ def _apply_docx_direct_run_style(run: DocxRun, style: StyleDescriptor) -> None:
724
+ if style.bold is not None:
725
+ run.bold = style.bold
726
+ if style.italic is not None:
727
+ run.italic = style.italic
728
+ if style.underline is not None:
729
+ run.underline = style.underline
730
+ if style.font_family is not None:
731
+ _set_docx_run_font_family(run, style.font_family)
732
+ if style.font_size_pt is not None:
733
+ run.font.size = Pt(float(style.font_size_pt))
734
+ if style.font_color_rgb is not None:
735
+ run.font.color.rgb = RGBColor.from_string(style.font_color_rgb)
736
+
737
+
738
+ def _set_docx_run_font_family(run: DocxRun, font_family: str) -> None:
739
+ run.font.name = font_family
740
+ r_pr = run._element.get_or_add_rPr() # noqa: SLF001 - CJK fonts require raw run OOXML.
741
+ r_fonts = r_pr.find(qn("w:rFonts"))
742
+ if r_fonts is None:
743
+ r_fonts = OxmlElement("w:rFonts")
744
+ r_pr.insert(0, r_fonts)
745
+ for attribute_name in ("w:ascii", "w:hAnsi", "w:eastAsia", "w:cs"):
746
+ r_fonts.set(qn(attribute_name), font_family)
747
+
748
+
749
+ def _apply_xlsx_operation(workbook: openpyxl.Workbook, operation: DocumentPatchOperation) -> None:
750
+ if operation.operation_type is OperationType.set_table_cell:
751
+ worksheet, cell_ref = _xlsx_cell_target(workbook, operation.target_path)
752
+ _ensure_xlsx_cell_editable(worksheet, cell_ref)
753
+ worksheet[cell_ref].value = _office_scalar(operation.value)
754
+ return
755
+ if operation.operation_type is OperationType.set_cell_style:
756
+ worksheet, cell_ref = _xlsx_cell_target(workbook, operation.target_path)
757
+ _ensure_xlsx_cell_editable(worksheet, cell_ref)
758
+ _apply_xlsx_cell_style(worksheet[cell_ref], operation.style)
759
+ return
760
+ if operation.operation_type is OperationType.set_document_metadata:
761
+ _set_xlsx_metadata(workbook, operation)
762
+ return
763
+ raise ValueError(f"Unsupported XLSX operation: {operation.operation_type.value}")
764
+
765
+
766
+ def _xlsx_cell_target(
767
+ workbook: openpyxl.Workbook,
768
+ target_path: str,
769
+ ) -> tuple[Worksheet, str]:
770
+ match = _XLSX_CELL_RE.match(target_path)
771
+ if match is None:
772
+ raise ValueError(f"Unsupported XLSX cell target: {target_path}")
773
+ sheet_name = match.group("sheet")
774
+ if sheet_name not in workbook.sheetnames:
775
+ raise ValueError(f"XLSX sheet does not exist: {sheet_name}")
776
+ return workbook[sheet_name], match.group("cell").upper()
777
+
778
+
779
+ def _ensure_xlsx_cell_editable(worksheet: Worksheet, cell_ref: str) -> None:
780
+ merged_ranges = worksheet.merged_cells.ranges
781
+ for merged_range in merged_ranges:
782
+ if cell_ref in merged_range and cell_ref != str(merged_range).split(":", 1)[0]:
783
+ raise ValueError(f"Cannot edit non-anchor merged cell: {cell_ref}")
784
+
785
+
786
+ def _apply_xlsx_cell_style(cell: Cell, style: StyleDescriptor | None) -> None:
787
+ if style is None:
788
+ raise ValueError("XLSX cell style operation requires style")
789
+ cell.font = _xlsx_font_with_style(cell, style)
790
+ _apply_xlsx_fill_alignment_and_number_format(cell, style)
791
+
792
+
793
+ def _xlsx_font_with_style(cell: Cell, style: StyleDescriptor) -> Font:
794
+ font = copy(cell.font)
795
+ if style.bold is not None:
796
+ font.bold = style.bold
797
+ if style.italic is not None:
798
+ font.italic = style.italic
799
+ if style.underline is not None:
800
+ font.underline = "single" if style.underline else None
801
+ if style.font_family is not None:
802
+ font.name = style.font_family
803
+ if style.font_size_pt is not None:
804
+ font.sz = float(style.font_size_pt)
805
+ if style.font_color_rgb is not None:
806
+ font.color = style.font_color_rgb
807
+ return cast(Font, font)
808
+
809
+
810
+ def _apply_xlsx_fill_alignment_and_number_format(cell: Cell, style: StyleDescriptor) -> None:
811
+ if style.fill_color_rgb is not None:
812
+ cell.fill = PatternFill("solid", fgColor=style.fill_color_rgb)
813
+ if style.alignment is not None:
814
+ cell.alignment = Alignment(horizontal=style.alignment)
815
+ if style.number_format is not None:
816
+ cell.number_format = style.number_format
817
+
818
+
819
+ def _set_xlsx_metadata(workbook: openpyxl.Workbook, operation: DocumentPatchOperation) -> None:
820
+ property_name = operation.target_path.rsplit("/", 1)[-1]
821
+ if property_name not in {"creator", "title", "subject", "description", "keywords"}:
822
+ raise ValueError(f"Unsupported XLSX metadata target: {operation.target_path}")
823
+ setattr(workbook.properties, property_name, _string_value(operation.value))
824
+
825
+
826
+ def _cell_has_non_default_style(cell: object) -> bool:
827
+ return bool(getattr(cell, "has_style", False))
828
+
829
+
830
+ def _xlsx_style_descriptor(cell: Cell, source_path: str) -> StyleDescriptor:
831
+ font = cell.font
832
+ fill = cell.fill
833
+ fill_color = getattr(fill, "fgColor", None)
834
+ fill_rgb = getattr(fill_color, "rgb", None)
835
+ if isinstance(fill_rgb, str) and len(fill_rgb) == 8:
836
+ fill_rgb = fill_rgb[-6:]
837
+ return StyleDescriptor(
838
+ style_id=f"xlsx-style-{source_path.strip('/').replace('/', '-')}",
839
+ target_path=source_path,
840
+ font_family=getattr(font, "name", None),
841
+ font_size_pt=Decimal(str(getattr(font, "sz", 0))) if getattr(font, "sz", None) else None,
842
+ bold=getattr(font, "bold", None),
843
+ italic=getattr(font, "italic", None),
844
+ fill_color_rgb=fill_rgb if isinstance(fill_rgb, str) and len(fill_rgb) == 6 else None,
845
+ number_format=getattr(cell, "number_format", None),
846
+ )
847
+
848
+
849
+ def _apply_pptx_operation(
850
+ presentation: PptxPresentation,
851
+ operation: DocumentPatchOperation,
852
+ ) -> None:
853
+ if operation.operation_type in {OperationType.replace_text, OperationType.set_field_value}:
854
+ _set_pptx_text(presentation, operation.target_path, _string_value(operation.value))
855
+ return
856
+ if operation.operation_type is OperationType.set_table_cell:
857
+ table, row_index, cell_index = _pptx_table_cell(presentation, operation.target_path)
858
+ table.cell(row_index, cell_index).text = _string_value(operation.value)
859
+ return
860
+ if operation.operation_type is OperationType.set_document_metadata:
861
+ _set_pptx_metadata(presentation, operation)
862
+ return
863
+ raise ValueError(f"Unsupported PPTX operation: {operation.operation_type.value}")
864
+
865
+
866
+ def _set_pptx_text(
867
+ presentation: PptxPresentation,
868
+ target_path: str,
869
+ value: str,
870
+ ) -> None:
871
+ if target_path.endswith("/placeholders/title"):
872
+ slide = _pptx_slide(presentation, target_path)
873
+ if slide.shapes.title is None:
874
+ raise ValueError(f"PPTX title placeholder not found: {target_path}")
875
+ slide.shapes.title.text = value
876
+ return
877
+ match = _PPTX_SHAPE_TEXT_RE.match(target_path)
878
+ if match is None:
879
+ raise ValueError(f"Unsupported PPTX text target: {target_path}")
880
+ slide = presentation.slides[int(match.group("slide")) - 1]
881
+ shape = slide.shapes[int(match.group("shape")) - 1]
882
+ if not getattr(shape, "has_text_frame", False):
883
+ raise ValueError(f"PPTX shape has no text frame: {target_path}")
884
+ shape.text = value
885
+
886
+
887
+ def _pptx_slide(presentation: PptxPresentation, target_path: str) -> PptxSlide:
888
+ match = re.match(r"^/slides/(?P<slide>\d+)/", target_path)
889
+ if match is None:
890
+ raise ValueError(f"Unsupported PPTX slide target: {target_path}")
891
+ return cast(PptxSlide, presentation.slides[int(match.group("slide")) - 1])
892
+
893
+
894
+ def _pptx_table_cell(
895
+ presentation: PptxPresentation,
896
+ target_path: str,
897
+ ) -> tuple[PptxTable, int, int]:
898
+ match = _PPTX_TABLE_CELL_RE.match(target_path)
899
+ if match is None:
900
+ raise ValueError(f"Unsupported PPTX table target: {target_path}")
901
+ slide = presentation.slides[int(match.group("slide")) - 1]
902
+ table_ordinal = int(match.group("table"))
903
+ table_shape_count = 0
904
+ for shape in slide.shapes:
905
+ if getattr(shape, "has_table", False):
906
+ table_shape_count += 1
907
+ if table_shape_count == table_ordinal:
908
+ return (
909
+ cast(PptxTable, shape.table),
910
+ int(match.group("row")) - 1,
911
+ int(match.group("cell")) - 1,
912
+ )
913
+ raise ValueError(f"PPTX table not found: {target_path}")
914
+
915
+
916
+ def _set_pptx_metadata(presentation: PptxPresentation, operation: DocumentPatchOperation) -> None:
917
+ property_name = operation.target_path.rsplit("/", 1)[-1]
918
+ if property_name not in {"author", "category", "comments", "keywords", "subject", "title"}:
919
+ raise ValueError(f"Unsupported PPTX core metadata target: {operation.target_path}")
920
+ setattr(presentation.core_properties, property_name, _string_value(operation.value))
921
+
922
+
923
+ def _pptx_table_block(
924
+ table: PptxTable,
925
+ *,
926
+ slide_index: int,
927
+ table_index: int,
928
+ ) -> TableBlock:
929
+ cells: list[TableCell] = []
930
+ for row_index, row in enumerate(table.rows):
931
+ for column_index, cell in enumerate(row.cells):
932
+ cells.append(
933
+ TableCell(
934
+ row_index=row_index,
935
+ column_index=column_index,
936
+ text=cell.text,
937
+ source_path=(
938
+ f"/slides/{slide_index}/tables/{table_index}/rows/"
939
+ f"{row_index + 1}/cells/{column_index + 1}"
940
+ ),
941
+ )
942
+ )
943
+ return TableBlock(
944
+ block_id=f"pptx-slide-{slide_index:03d}-table-{table_index:03d}",
945
+ source_path=f"/slides/{slide_index}/tables/{table_index}",
946
+ cells=cells,
947
+ )
948
+
949
+
950
+ def _pptx_core_metadata(presentation: PptxPresentation) -> dict[str, MetadataValue]:
951
+ properties = presentation.core_properties
952
+ metadata: dict[str, MetadataValue] = {
953
+ "engine_id": "python-pptx",
954
+ "format": "pptx",
955
+ "slide_count": len(presentation.slides),
956
+ }
957
+ for property_name in (
958
+ "author",
959
+ "category",
960
+ "comments",
961
+ "created",
962
+ "keywords",
963
+ "last_modified_by",
964
+ "modified",
965
+ "revision",
966
+ "subject",
967
+ "title",
968
+ "version",
969
+ ):
970
+ value = getattr(properties, property_name)
971
+ if _metadata_value_is_present(value):
972
+ metadata[f"core_{property_name}"] = value
973
+ return metadata
974
+
975
+
976
+ def _slide_index_from_path(source_path: str) -> int:
977
+ match = re.match(r"^/slides/(?P<slide>\d+)/", source_path)
978
+ return int(match.group("slide")) if match is not None else 1
979
+
980
+
981
+ def _save_workbook_to_bytes(workbook: openpyxl.Workbook) -> bytes:
982
+ from io import BytesIO
983
+
984
+ buffer = BytesIO()
985
+ workbook.save(buffer)
986
+ return buffer.getvalue()
987
+
988
+
989
+ def _save_to_bytes(document: _OfficeSaveable) -> bytes:
990
+ from io import BytesIO
991
+
992
+ buffer = BytesIO()
993
+ document.save(buffer)
994
+ return buffer.getvalue()
995
+
996
+
997
+ def _svg_page(lines: list[str], *, title: str) -> bytes:
998
+ escaped_title = html.escape(title)
999
+ text_nodes = []
1000
+ for index, line in enumerate(lines[:40], start=1):
1001
+ text_nodes.append(
1002
+ f'<text x="48" y="{64 + index * 24}" font-family="Arial" '
1003
+ f'font-size="16">{html.escape(str(line))}</text>'
1004
+ )
1005
+ payload = (
1006
+ '<svg xmlns="http://www.w3.org/2000/svg" width="960" height="1240" '
1007
+ 'viewBox="0 0 960 1240">'
1008
+ '<rect width="960" height="1240" fill="#fff"/>'
1009
+ '<rect x="28" y="28" width="904" height="1184" fill="none" '
1010
+ 'stroke="#c7c7c7" stroke-width="2"/>'
1011
+ f'<text x="48" y="54" font-family="Arial" font-size="20" '
1012
+ f'font-weight="700">{escaped_title}</text>'
1013
+ f"{''.join(text_nodes)}"
1014
+ "</svg>"
1015
+ )
1016
+ return payload.encode("utf-8")
1017
+
1018
+
1019
+ def _string_value(value: ScalarValue) -> str:
1020
+ if value is None:
1021
+ return ""
1022
+ if isinstance(value, date | datetime):
1023
+ return value.isoformat()
1024
+ return str(value)
1025
+
1026
+
1027
+ def _office_scalar(value: ScalarValue) -> str | int | float | bool | date | datetime | None:
1028
+ if isinstance(value, Decimal):
1029
+ return float(value)
1030
+ return value