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.
- package/README.md +15 -2
- package/bin/ummaya +10 -1
- package/bun.lock +180 -244
- package/npm-shrinkwrap.json +760 -1760
- package/package.json +39 -22
- package/prompts/manifest.yaml +1 -1
- package/prompts/system_v1.md +1 -0
- package/pyproject.toml +27 -2
- package/specs/2803-document-production-hardening/contracts/document-tools.schema.json +1043 -0
- package/src/ummaya/_canonical/__init__.py +2 -0
- package/src/ummaya/_canonical/baselines.yaml +113 -0
- package/src/ummaya/engine/engine.py +29 -132
- package/src/ummaya/evidence/__init__.py +21 -2
- package/src/ummaya/evidence/dataset_contract.py +193 -0
- package/src/ummaya/evidence/document_authoring_cases.py +33 -0
- package/src/ummaya/evidence/document_harness.py +313 -0
- package/src/ummaya/evidence/document_viewer_ux.py +391 -0
- package/src/ummaya/evidence/gates.py +70 -0
- package/src/ummaya/evidence/json_types.py +20 -0
- package/src/ummaya/evidence/models.py +88 -1
- package/src/ummaya/evidence/output_payload.py +89 -0
- package/src/ummaya/evidence/payload_documents.py +233 -0
- package/src/ummaya/evidence/route_contracts.py +224 -0
- package/src/ummaya/evidence/route_helpers.py +150 -0
- package/src/ummaya/evidence/runner.py +81 -212
- package/src/ummaya/evidence/source_provenance.py +246 -0
- package/src/ummaya/evidence/source_provenance_redaction.py +176 -0
- package/src/ummaya/evidence/tool_layer.py +39 -0
- package/src/ummaya/evidence/tool_layer_models.py +151 -0
- package/src/ummaya/ipc/adapter_manifest_emitter.py +26 -10
- package/src/ummaya/ipc/document_intent_normalization.py +185 -0
- package/src/ummaya/ipc/frame_schema.py +5 -5
- package/src/ummaya/ipc/route_diagnostics.py +73 -0
- package/src/ummaya/ipc/stdio.py +1109 -477
- package/src/ummaya/llm/client.py +102 -3
- package/src/ummaya/llm/config.py +8 -3
- package/src/ummaya/primitives/__init__.py +6 -2
- package/src/ummaya/primitives/delegation.py +1 -1
- package/src/ummaya/primitives/document.py +28 -0
- package/src/ummaya/settings.py +0 -3
- package/src/ummaya/tools/discovery_bridge.py +17 -1
- package/src/ummaya/tools/documents/__init__.py +297 -0
- package/src/ummaya/tools/documents/adapter_registry.py +487 -0
- package/src/ummaya/tools/documents/archive_container_probe.py +167 -0
- package/src/ummaya/tools/documents/artifact_store.py +454 -0
- package/src/ummaya/tools/documents/authoring.py +283 -0
- package/src/ummaya/tools/documents/baselines.py +132 -0
- package/src/ummaya/tools/documents/capability.py +331 -0
- package/src/ummaya/tools/documents/contracts.py +112 -0
- package/src/ummaya/tools/documents/conversion.py +521 -0
- package/src/ummaya/tools/documents/diff.py +275 -0
- package/src/ummaya/tools/documents/engines.py +163 -0
- package/src/ummaya/tools/documents/evaluation.py +291 -0
- package/src/ummaya/tools/documents/explicit_values.py +108 -0
- package/src/ummaya/tools/documents/fixtures.py +174 -0
- package/src/ummaya/tools/documents/format_completion_audit.py +471 -0
- package/src/ummaya/tools/documents/formats/__init__.py +2 -0
- package/src/ummaya/tools/documents/formats/archive.py +528 -0
- package/src/ummaya/tools/documents/formats/base.py +41 -0
- package/src/ummaya/tools/documents/formats/code_file.py +211 -0
- package/src/ummaya/tools/documents/formats/data_file.py +272 -0
- package/src/ummaya/tools/documents/formats/hwp.py +284 -0
- package/src/ummaya/tools/documents/formats/hwpx.py +1837 -0
- package/src/ummaya/tools/documents/formats/odf.py +435 -0
- package/src/ummaya/tools/documents/formats/ooxml.py +1030 -0
- package/src/ummaya/tools/documents/formats/passive.py +766 -0
- package/src/ummaya/tools/documents/formats/pdf.py +702 -0
- package/src/ummaya/tools/documents/formats/text_web.py +268 -0
- package/src/ummaya/tools/documents/hwp_conversion_probe.py +178 -0
- package/src/ummaya/tools/documents/hwp_direct_candidate.py +141 -0
- package/src/ummaya/tools/documents/inspection.py +289 -0
- package/src/ummaya/tools/documents/intake.py +1079 -0
- package/src/ummaya/tools/documents/legacy_office_promotion_probe.py +366 -0
- package/src/ummaya/tools/documents/models.py +1598 -0
- package/src/ummaya/tools/documents/odf_promotion_probe.py +167 -0
- package/src/ummaya/tools/documents/orchestrator.py +96 -0
- package/src/ummaya/tools/documents/passive_capability_probe.py +251 -0
- package/src/ummaya/tools/documents/patch.py +170 -0
- package/src/ummaya/tools/documents/pdfa_conformance.py +284 -0
- package/src/ummaya/tools/documents/pdfa_promotion_probe.py +198 -0
- package/src/ummaya/tools/documents/permissions.py +110 -0
- package/src/ummaya/tools/documents/planner.py +616 -0
- package/src/ummaya/tools/documents/registry.py +2733 -0
- package/src/ummaya/tools/documents/render.py +978 -0
- package/src/ummaya/tools/documents/render_comparison.py +113 -0
- package/src/ummaya/tools/documents/render_comparison_models.py +74 -0
- package/src/ummaya/tools/documents/render_comparison_regions.py +73 -0
- package/src/ummaya/tools/documents/render_comparison_style.py +161 -0
- package/src/ummaya/tools/documents/reread.py +157 -0
- package/src/ummaya/tools/documents/runtime_authoring.py +244 -0
- package/src/ummaya/tools/documents/runtime_authoring_bundle.py +76 -0
- package/src/ummaya/tools/documents/scorecard.py +184 -0
- package/src/ummaya/tools/documents/socratic_planner.py +193 -0
- package/src/ummaya/tools/documents/style.py +48 -0
- package/src/ummaya/tools/documents/tool_defs.py +523 -0
- package/src/ummaya/tools/documents/validate.py +347 -0
- package/src/ummaya/tools/executor.py +29 -0
- package/src/ummaya/tools/live_proxy.py +0 -3
- package/src/ummaya/tools/models.py +5 -1
- package/src/ummaya/tools/register_all.py +8 -0
- package/src/ummaya/tools/registry.py +10 -1
- package/src/ummaya/tools/routing/__init__.py +59 -0
- package/src/ummaya/tools/routing/builder.py +105 -0
- package/src/ummaya/tools/routing/cards.py +29 -0
- package/src/ummaya/tools/routing/decision_service.py +534 -0
- package/src/ummaya/tools/routing/decision_types.py +74 -0
- package/src/ummaya/tools/routing/feasibility.py +122 -0
- package/src/ummaya/tools/routing/intent.py +17 -0
- package/src/ummaya/tools/routing/intent_extractor.py +207 -0
- package/src/ummaya/tools/routing/intent_patterns.py +160 -0
- package/src/ummaya/tools/routing/intent_public_data.py +150 -0
- package/src/ummaya/tools/routing/intent_types.py +48 -0
- package/src/ummaya/tools/routing/lint.py +78 -0
- package/src/ummaya/tools/routing/metadata.py +174 -0
- package/src/ummaya/tools/routing/projection.py +340 -0
- package/src/ummaya/tools/routing/retrieval_policy.py +629 -0
- package/src/ummaya/tools/routing/schema.py +81 -0
- package/src/ummaya/tools/routing/types.py +96 -0
- package/src/ummaya/tools/routing_index.py +2 -2
- package/src/ummaya/tools/search.py +34 -746
- package/tests/fixtures/documents/public_forms/baselines.yaml +113 -0
- package/tui/bun.lock +126 -305
- package/tui/package.json +35 -22
- package/tui/src/.cc-byte-identical-whitelist.yaml +266 -0
- package/tui/src/QueryEngine.ts +12 -8
- package/tui/src/bridge/inboundAttachments.ts +3 -3
- package/tui/src/cli/handlers/auth.ts +3 -12
- package/tui/src/cli/handlers/mcp.tsx +0 -1
- package/tui/src/cli/print.ts +8 -9
- package/tui/src/commands/insights.ts +1 -1
- package/tui/src/commands/install-github-app/types.ts +8 -30
- package/tui/src/commands/plugin/types.ts +6 -28
- package/tui/src/commands/plugin/unifiedTypes.ts +4 -26
- package/tui/src/commands/rename/generateSessionName.ts +1 -1
- package/tui/src/components/Feedback.tsx +1 -1
- package/tui/src/components/LogoV2/EmergencyTip.tsx +11 -2
- package/tui/src/components/LogoV2/WelcomeV2.tsx +1 -3
- package/tui/src/components/ScrollKeybindingHandler.tsx +6 -6
- package/tui/src/components/Spinner/types.ts +6 -28
- package/tui/src/components/agents/generateAgent.ts +1 -1
- package/tui/src/components/agents/new-agent-creation/types.ts +4 -26
- package/tui/src/components/config/EnvSecretIsolatedEditor.tsx +1 -1
- package/tui/src/components/mcp/types.ts +16 -38
- package/tui/src/components/messages/AssistantToolUseMessage.tsx +3 -2
- package/tui/src/components/messages/UserCrossSessionMessage.ts +16 -4
- package/tui/src/components/messages/UserForkBoilerplateMessage.ts +16 -4
- package/tui/src/components/messages/UserGitHubWebhookMessage.ts +16 -4
- package/tui/src/components/messages/UserToolResultMessage/utils.tsx +3 -2
- package/tui/src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.ts +9 -4
- package/tui/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.ts +9 -4
- package/tui/src/components/primitive/DocumentSocraticReviewBlock.tsx +129 -0
- package/tui/src/components/primitive/DocumentToolResultCard.tsx +224 -0
- package/tui/src/components/primitive/documentSocraticReview.ts +215 -0
- package/tui/src/components/primitive/index.tsx +43 -1
- package/tui/src/components/primitive/types.ts +137 -0
- package/tui/src/components/ui/option.ts +4 -26
- package/tui/src/constants/common.ts +0 -2
- package/tui/src/constants/prompts.ts +4 -3
- package/tui/src/constants/querySource.ts +4 -26
- package/tui/src/entrypoints/sdk/controlTypes.ts +26 -48
- package/tui/src/entrypoints/sdk/coreTypes.generated.ts +3 -25
- package/tui/src/entrypoints/sdk/runtimeTypes.ts +38 -60
- package/tui/src/entrypoints/sdk/sdkUtilityTypes.ts +4 -26
- package/tui/src/entrypoints/sdk/settingsTypes.generated.ts +3 -25
- package/tui/src/entrypoints/sdk/toolTypes.ts +3 -25
- package/tui/src/hooks/toolPermission/handlers/interactiveHandler.ts +10 -0
- package/tui/src/hooks/useApiKeyVerification.ts +1 -1
- package/tui/src/hooks/useVirtualScroll.ts +1 -1
- package/tui/src/ink/ink.tsx +33 -14
- package/tui/src/ink/reconciler.ts +2 -3
- package/tui/src/ink/render-to-screen.ts +30 -10
- package/tui/src/ipc/bridge.ts +62 -15
- package/tui/src/ipc/bridgeSingleton.ts +5 -1
- package/tui/src/ipc/codec.ts +3 -3
- package/tui/src/ipc/frames.generated.ts +12 -12
- package/tui/src/ipc/llmClient.ts +151 -27
- package/tui/src/ipc/schema/frame.schema.json +1 -1
- package/tui/src/keybindings/defaultBindings.ts +4 -0
- package/tui/src/main.tsx +32 -15
- package/tui/src/native-ts/file-index/index.ts +33 -3
- package/tui/src/observability/surface.ts +2 -2
- package/tui/src/probes/toolRegistryProbe.tsx +3 -1
- package/tui/src/projectOnboardingState.ts +7 -6
- package/tui/src/query/chatMessageTypes.ts +18 -0
- package/tui/src/query/chatMessagesBuilder.ts +1 -1
- package/tui/src/query/deps.ts +1 -1
- package/tui/src/query/messageGuards.ts +106 -0
- package/tui/src/query/publicDataTerminalRepair.ts +384 -0
- package/tui/src/query/run.ts +1075 -0
- package/tui/src/query/supportBoundary.ts +168 -0
- package/tui/src/query/toolResultErrors.ts +103 -0
- package/tui/src/query/toolRunner.ts +687 -0
- package/tui/src/query/unavailableToolRepair.ts +118 -0
- package/tui/src/query.ts +9 -2186
- package/tui/src/screens/REPL.tsx +40 -29
- package/tui/src/services/api/adapterManifest.ts +4 -0
- package/tui/src/services/api/backendChat/events.ts +117 -0
- package/tui/src/services/api/backendChat/finalMessage.ts +40 -0
- package/tui/src/services/api/backendChat/frame.ts +9 -0
- package/tui/src/services/api/backendChat/streaming.ts +430 -0
- package/tui/src/services/api/backendChat/types.ts +62 -0
- package/tui/src/services/api/backendChat.ts +1 -0
- package/tui/src/services/api/client.ts +65 -2
- package/tui/src/services/api/errorUtils.ts +5 -5
- package/tui/src/services/api/errors.ts +1 -1
- package/tui/src/services/api/logging.ts +1 -1
- package/tui/src/services/api/ummaya/evidence.ts +194 -0
- package/tui/src/services/api/ummaya/messages.ts +255 -0
- package/tui/src/services/api/ummaya/nonStreaming.ts +66 -0
- package/tui/src/services/api/ummaya/provider.ts +200 -0
- package/tui/src/services/api/ummaya/reasoning.ts +24 -0
- package/tui/src/services/api/ummaya/request.ts +200 -0
- package/tui/src/services/api/ummaya/selectionContext.ts +240 -0
- package/tui/src/services/api/ummaya/streaming.ts +365 -0
- package/tui/src/services/api/ummaya/streamingPayload.ts +129 -0
- package/tui/src/services/api/ummaya/streamingReader.ts +40 -0
- package/tui/src/services/api/ummaya/toolSelection.ts +217 -0
- package/tui/src/services/api/ummaya/types.ts +110 -0
- package/tui/src/services/api/ummaya/usage.ts +30 -0
- package/tui/src/services/api/ummaya.ts +26 -418
- package/tui/src/services/api/withRetry.ts +1 -1
- package/tui/src/services/awaySummary.ts +2 -2
- package/tui/src/services/claudeAiLimits.ts +1 -1
- package/tui/src/services/compact/autoCompact.ts +1 -1
- package/tui/src/services/compact/compact.ts +1 -1
- package/tui/src/services/lsp/types.ts +8 -30
- package/tui/src/services/tips/types.ts +6 -28
- package/tui/src/services/tokenEstimation.ts +1 -1
- package/tui/src/services/toolRegistry/bootGuard.ts +5 -5
- package/tui/src/services/toolUseSummary/toolUseSummaryGenerator.ts +1 -1
- package/tui/src/services/tools/toolExecution.ts +94 -1
- package/tui/src/store/pendingPermissionSlot.ts +1 -1
- package/tui/src/store/session-store.ts +10 -36
- package/tui/src/stubs/any-stub.ts +15 -10
- package/tui/src/stubs/color-diff-napi.ts +37 -23
- package/tui/src/stubs/globals.d.ts +3 -3
- package/tui/src/stubs/macro-preload.ts +23 -12
- package/tui/src/tools/AdapterTool/AdapterTool.ts +1207 -714
- package/tui/src/tools/AdapterTool/routeDiagnostics.ts +75 -0
- package/tui/src/tools/AgentTool/AgentTool.tsx +84 -1371
- package/tui/src/tools/AgentTool/agentToolHandoff.ts +114 -0
- package/tui/src/tools/AgentTool/agentToolPartialResult.ts +16 -0
- package/tui/src/tools/AgentTool/agentToolProgress.ts +32 -0
- package/tui/src/tools/AgentTool/agentToolResolver.ts +161 -0
- package/tui/src/tools/AgentTool/agentToolResult.ts +163 -0
- package/tui/src/tools/AgentTool/agentToolUtils.ts +14 -686
- package/tui/src/tools/AgentTool/asyncAgentLifecycle.ts +208 -0
- package/tui/src/tools/AgentTool/asyncLifecycle.ts +153 -0
- package/tui/src/tools/AgentTool/backgroundedCompletion.ts +126 -0
- package/tui/src/tools/AgentTool/backgroundedLifecycle.ts +174 -0
- package/tui/src/tools/AgentTool/foregroundBackground.ts +83 -0
- package/tui/src/tools/AgentTool/foregroundDrain.tsx +133 -0
- package/tui/src/tools/AgentTool/foregroundFinalize.ts +98 -0
- package/tui/src/tools/AgentTool/foregroundLifecycle.tsx +237 -0
- package/tui/src/tools/AgentTool/foregroundProgress.tsx +169 -0
- package/tui/src/tools/AgentTool/foregroundTask.ts +89 -0
- package/tui/src/tools/AgentTool/forkSubagent.ts +1 -12
- package/tui/src/tools/AgentTool/forkSubagentGate.ts +34 -0
- package/tui/src/tools/AgentTool/launchRouting.ts +203 -0
- package/tui/src/tools/AgentTool/lifecycle.ts +244 -0
- package/tui/src/tools/AgentTool/mcpRouting.ts +73 -0
- package/tui/src/tools/AgentTool/orchestrationSupport.ts +70 -0
- package/tui/src/tools/AgentTool/permissions.ts +39 -0
- package/tui/src/tools/AgentTool/promptSetup.ts +181 -0
- package/tui/src/tools/AgentTool/remoteRouting.ts +62 -0
- package/tui/src/tools/AgentTool/resultMapping.ts +116 -0
- package/tui/src/tools/AgentTool/resumeAgent.ts +39 -107
- package/tui/src/tools/AgentTool/resumeAgentHelpers.ts +140 -0
- package/tui/src/tools/AgentTool/runAgent.ts +1 -1
- package/tui/src/tools/AgentTool/runtimeConfig.ts +57 -0
- package/tui/src/tools/AgentTool/schemas.ts +196 -0
- package/tui/src/tools/AgentTool/sourceVerificationPropagation.ts +263 -0
- package/tui/src/tools/AgentTool/worktreeLifecycle.ts +105 -0
- package/tui/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx +174 -202
- package/tui/src/tools/BashTool/BashTool.tsx +71 -1072
- package/tui/src/tools/BashTool/bashCommandHelpers.ts +12 -12
- package/tui/src/tools/BashTool/bashPermissions/astPreflight.ts +173 -0
- package/tui/src/tools/BashTool/bashPermissions/classifierChecks.ts +199 -0
- package/tui/src/tools/BashTool/bashPermissions/compoundGuards.ts +53 -0
- package/tui/src/tools/BashTool/bashPermissions/constants.ts +99 -0
- package/tui/src/tools/BashTool/bashPermissions/index.ts +38 -0
- package/tui/src/tools/BashTool/bashPermissions/legacyMisparsing.ts +62 -0
- package/tui/src/tools/BashTool/bashPermissions/main.ts +135 -0
- package/tui/src/tools/BashTool/bashPermissions/normalizedCommands.ts +33 -0
- package/tui/src/tools/BashTool/bashPermissions/operatorFlow.ts +98 -0
- package/tui/src/tools/BashTool/bashPermissions/permissionChecks.ts +200 -0
- package/tui/src/tools/BashTool/bashPermissions/prefixSuggestions.ts +88 -0
- package/tui/src/tools/BashTool/bashPermissions/promptClassifierRules.ts +125 -0
- package/tui/src/tools/BashTool/bashPermissions/ruleDelegates.ts +19 -0
- package/tui/src/tools/BashTool/bashPermissions/ruleMatching.ts +145 -0
- package/tui/src/tools/BashTool/bashPermissions/sandboxAutoAllow.ts +75 -0
- package/tui/src/tools/BashTool/bashPermissions/subcommandFlow.ts +205 -0
- package/tui/src/tools/BashTool/bashPermissions/subcommandGuards.ts +73 -0
- package/tui/src/tools/BashTool/bashPermissions/subcommandResultHelpers.ts +116 -0
- package/tui/src/tools/BashTool/bashPermissions/types.ts +26 -0
- package/tui/src/tools/BashTool/bashPermissions/wrapperStripping.ts +139 -0
- package/tui/src/tools/BashTool/bashPermissions.ts +26 -2621
- package/tui/src/tools/BashTool/call.ts +202 -0
- package/tui/src/tools/BashTool/callLoader.ts +35 -0
- package/tui/src/tools/BashTool/commandClassification.ts +151 -0
- package/tui/src/tools/BashTool/commandClassificationLoader.ts +40 -0
- package/tui/src/tools/BashTool/cwdReset.ts +33 -0
- package/tui/src/tools/BashTool/lineTruncation.ts +11 -0
- package/tui/src/tools/BashTool/modeValidation.ts +13 -1
- package/tui/src/tools/BashTool/outputPersistence.ts +42 -0
- package/tui/src/tools/BashTool/permissionClassification.ts +66 -0
- package/tui/src/tools/BashTool/permissionLoader.ts +44 -0
- package/tui/src/tools/BashTool/resultLoader.ts +29 -0
- package/tui/src/tools/BashTool/resultMapping.ts +83 -0
- package/tui/src/tools/BashTool/sandboxPolicy.ts +79 -0
- package/tui/src/tools/BashTool/schemas.ts +65 -0
- package/tui/src/tools/BashTool/sedEditExecution.ts +59 -0
- package/tui/src/tools/BashTool/shellExecution.tsx +245 -0
- package/tui/src/tools/BashTool/shellOutputUtils.ts +85 -0
- package/tui/src/tools/BashTool/shellPermissionGauntlet.ts +97 -0
- package/tui/src/tools/BashTool/uiLoader.ts +37 -0
- package/tui/src/tools/BriefTool/upload.ts +1 -1
- package/tui/src/tools/CalculatorTool/parser.ts +2 -2
- package/tui/src/tools/DocumentPrimitive/DocumentPrimitive.ts +262 -0
- package/tui/src/tools/DocumentPrimitive/dispatchNormalization.ts +270 -0
- package/tui/src/tools/DocumentPrimitive/documentDestinationPath.ts +18 -0
- package/tui/src/tools/DocumentPrimitive/documentMutationGuard.ts +22 -0
- package/tui/src/tools/DocumentPrimitive/documentPatchNormalization.ts +248 -0
- package/tui/src/tools/DocumentPrimitive/documentSourceVerification.ts +245 -0
- package/tui/src/tools/DocumentPrimitive/documentSourceVerificationFields.ts +103 -0
- package/tui/src/tools/DocumentPrimitive/modelVisibleOutput.ts +40 -0
- package/tui/src/tools/DocumentPrimitive/prompt.ts +35 -0
- package/tui/src/tools/FileEditTool/FileEditTool.ts +9 -507
- package/tui/src/tools/FileEditTool/call.ts +228 -0
- package/tui/src/tools/FileEditTool/validateInput.ts +196 -0
- package/tui/src/tools/FileReadTool/imageProcessor.ts +13 -0
- package/tui/src/tools/FileWriteTool/FileWriteTool.ts +7 -300
- package/tui/src/tools/FileWriteTool/call.ts +223 -0
- package/tui/src/tools/FileWriteTool/validateInput.ts +80 -0
- package/tui/src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts +19 -3
- package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +25 -32
- package/tui/src/tools/LookupPrimitive/prompt.ts +0 -2
- package/tui/src/tools/MCPTool/trustPolicy.ts +118 -0
- package/tui/src/tools/McpAuthTool/McpAuthTool.ts +21 -3
- package/tui/src/tools/NotebookEditTool/NotebookEditTool.ts +7 -326
- package/tui/src/tools/NotebookEditTool/call.ts +254 -0
- package/tui/src/tools/NotebookEditTool/notebookModel.ts +51 -0
- package/tui/src/tools/NotebookEditTool/validateInput.ts +142 -0
- package/tui/src/tools/PowerShellTool/PowerShellTool.tsx +46 -937
- package/tui/src/tools/PowerShellTool/acceptEditsCommandValidation.ts +162 -0
- package/tui/src/tools/PowerShellTool/call.ts +179 -0
- package/tui/src/tools/PowerShellTool/callLoader.ts +37 -0
- package/tui/src/tools/PowerShellTool/commandClassification.ts +86 -0
- package/tui/src/tools/PowerShellTool/modeValidation.ts +25 -332
- package/tui/src/tools/PowerShellTool/outputPersistence.ts +42 -0
- package/tui/src/tools/PowerShellTool/permissionClassification.ts +28 -0
- package/tui/src/tools/PowerShellTool/resultLoader.ts +31 -0
- package/tui/src/tools/PowerShellTool/resultMapping.ts +75 -0
- package/tui/src/tools/PowerShellTool/schemas.ts +40 -0
- package/tui/src/tools/PowerShellTool/shellExecution.tsx +258 -0
- package/tui/src/tools/PowerShellTool/symlinkModeValidation.ts +44 -0
- package/tui/src/tools/PowerShellTool/uiLoader.ts +37 -0
- package/tui/src/tools/PowerShellTool/validation.ts +39 -0
- package/tui/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts +19 -3
- package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +1 -11
- package/tui/src/tools/ResolveLocationPrimitive/prompt.ts +2 -6
- package/tui/src/tools/SkillTool/SkillTool.ts +2 -2
- package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +27 -10
- package/tui/src/tools/TaskCreateTool/TaskCreateTool.ts +16 -2
- package/tui/src/tools/TaskGetTool/TaskGetTool.ts +23 -3
- package/tui/src/tools/TaskListTool/TaskListTool.ts +22 -4
- package/tui/src/tools/TaskOutputTool/TaskOutputTool.tsx +46 -547
- package/tui/src/tools/TaskOutputTool/lookup.ts +216 -0
- package/tui/src/tools/TaskOutputTool/render.tsx +257 -0
- package/tui/src/tools/TaskOutputTool/schemas.ts +55 -0
- package/tui/src/tools/TaskOutputTool/serialization.ts +36 -0
- package/tui/src/tools/TaskStopTool/TaskStopTool.ts +10 -0
- package/tui/src/tools/TaskUpdateTool/TaskUpdateTool.ts +14 -364
- package/tui/src/tools/TaskUpdateTool/completion.ts +62 -0
- package/tui/src/tools/TaskUpdateTool/schemas.ts +62 -0
- package/tui/src/tools/TaskUpdateTool/serialization.ts +46 -0
- package/tui/src/tools/TaskUpdateTool/statusUpdate.ts +247 -0
- package/tui/src/tools/TodoWriteTool/TodoWriteTool.ts +21 -2
- package/tui/src/tools/ToolSearchTool/ToolSearchTool.ts +21 -302
- package/tui/src/tools/ToolSearchTool/ccSupportTools.ts +223 -0
- package/tui/src/tools/ToolSearchTool/descriptionCache.ts +50 -0
- package/tui/src/tools/ToolSearchTool/keywordSearch.ts +216 -0
- package/tui/src/tools/ToolSearchTool/prompt.ts +10 -4
- package/tui/src/tools/ToolSearchTool/resultMapping.ts +30 -0
- package/tui/src/tools/ToolSearchTool/schemas.ts +30 -0
- package/tui/src/tools/ToolSearchTool/searchPool.ts +47 -0
- package/tui/src/tools/ToolSearchTool/supportIntentHints.ts +140 -0
- package/tui/src/tools/TranslateTool/TranslateTool.ts +1 -1
- package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +2 -1
- package/tui/src/tools/WebFetchTool/WebFetchTool.ts +43 -138
- package/tui/src/tools/WebFetchTool/call.ts +227 -0
- package/tui/src/tools/WebFetchTool/resolvedAddressSafety.ts +78 -0
- package/tui/src/tools/WebFetchTool/sourceVerification.ts +204 -0
- package/tui/src/tools/WebFetchTool/types.ts +23 -0
- package/tui/src/tools/WebFetchTool/urlSafety.ts +181 -0
- package/tui/src/tools/WebFetchTool/utils.ts +1 -1
- package/tui/src/tools/WebSearchTool/UI.tsx +0 -1
- package/tui/src/tools/WebSearchTool/WebSearchTool.ts +9 -313
- package/tui/src/tools/WebSearchTool/call.ts +33 -0
- package/tui/src/tools/WebSearchTool/responseMapping.ts +190 -0
- package/tui/src/tools/WebSearchTool/resultBlock.ts +47 -0
- package/tui/src/tools/WebSearchTool/schemas.ts +47 -0
- package/tui/src/tools/WebSearchTool/toolSchema.ts +12 -0
- package/tui/src/tools/WorkspaceToolAdapter/WorkspaceToolAdapter.ts +79 -0
- package/tui/src/tools/WorkspaceToolAdapter/allowedRootPolicy.ts +85 -0
- package/tui/src/tools/WorkspaceToolAdapter/documentFormatGuards.ts +73 -0
- package/tui/src/tools/WorkspaceToolAdapter/inputNormalization.ts +105 -0
- package/tui/src/tools/WorkspaceToolAdapter/mcpExposurePolicy.ts +64 -0
- package/tui/src/tools/WorkspaceToolAdapter/toolDefFactory.ts +215 -0
- package/tui/src/tools/WorkspaceToolAdapter/toolNames.ts +6 -0
- package/tui/src/tools/WorkspaceToolAdapter/workspacePolicy.ts +15 -0
- package/tui/src/tools/_shared/dispatchPrimitive.ts +6 -6
- package/tui/src/tools/_shared/documentChangeToPatch.ts +125 -0
- package/tui/src/tools/_shared/documentDispatchArguments.ts +87 -0
- package/tui/src/tools/_shared/documentPrimitiveTimeout.ts +13 -0
- package/tui/src/tools/_shared/documentToolResultRender.ts +98 -0
- package/tui/src/tools/_shared/pendingCallRegistry.ts +1 -6
- package/tui/src/tools/_shared/rootPrimitiveInput.ts +1 -0
- package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPatterns.ts +58 -0
- package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPrompt.ts +271 -0
- package/tui/src/tools/_shared/toolChoiceRepair/documentRepair.ts +452 -0
- package/tui/src/tools/_shared/toolChoiceRepair/messageAccess.ts +80 -0
- package/tui/src/tools/_shared/toolChoiceRepair/publicDataRepair.ts +92 -0
- package/tui/src/tools/_shared/toolChoiceRepair/supportRepair.ts +135 -0
- package/tui/src/tools/_shared/toolChoiceRepair.ts +55 -860
- package/tui/src/tools/shared/mockDisclaimer.ts +1 -1
- package/tui/src/tools.ts +39 -190
- package/tui/src/types/fileSuggestion.ts +4 -26
- package/tui/src/types/generated/events_mono/claude_code/v1/claude_code_internal_event.ts +186 -148
- package/tui/src/types/generated/events_mono/common/v1/auth.ts +25 -11
- package/tui/src/types/generated/events_mono/growthbook/v1/growthbook_experiment_event.ts +47 -30
- package/tui/src/types/generated/google/protobuf/timestamp.ts +21 -7
- package/tui/src/types/message.ts +80 -102
- package/tui/src/types/messageQueueTypes.ts +6 -28
- package/tui/src/types/notebook.ts +16 -38
- package/tui/src/types/statusLine.ts +4 -26
- package/tui/src/types/tools.ts +24 -46
- package/tui/src/types/utils.ts +6 -28
- package/tui/src/upstreamproxy/relay.ts +7 -3
- package/tui/src/upstreamproxy/upstreamproxy.ts +1 -1
- package/tui/src/utils/assistantMessageFactories.ts +9 -3
- package/tui/src/utils/auth.ts +129 -139
- package/tui/src/utils/bash/ast.ts +23 -23
- package/tui/src/utils/bash/bashParser.ts +5 -5
- package/tui/src/utils/billing.ts +1 -1
- package/tui/src/utils/claudeDesktop.ts +4 -4
- package/tui/src/utils/collapseReadSearch.ts +3 -3
- package/tui/src/utils/cronTasks.ts +1 -1
- package/tui/src/utils/execFileNoThrow.ts +1 -1
- package/tui/src/utils/filePersistence/types.ts +16 -38
- package/tui/src/utils/forkedAgent.ts +1 -1
- package/tui/src/utils/gracefulShutdown.ts +4 -4
- package/tui/src/utils/heapDumpService.ts +12 -8
- package/tui/src/utils/hooks/apiQueryHookHelper.ts +1 -1
- package/tui/src/utils/hooks/execPromptHook.ts +1 -1
- package/tui/src/utils/hooks/skillImprovement.ts +1 -1
- package/tui/src/utils/mcp/dateTimeParser.ts +1 -1
- package/tui/src/utils/messages.ts +18 -0
- package/tui/src/utils/migrateSessions.ts +3 -3
- package/tui/src/utils/model/model.ts +6 -6
- package/tui/src/utils/permissions/yoloClassifier.ts +1 -1
- package/tui/src/utils/plugins/headlessPluginInstall.ts +1 -1
- package/tui/src/utils/plugins/mcpPluginIntegration.ts +1 -1
- package/tui/src/utils/plugins/mcpbHandler.ts +1 -1
- package/tui/src/utils/plugins/pluginLoader.ts +8 -8
- package/tui/src/utils/protectedNamespace.ts +5 -3
- package/tui/src/utils/rawJsonToolCall.ts +242 -0
- package/tui/src/utils/ripgrep.ts +16 -7
- package/tui/src/utils/sessionTitle.ts +1 -1
- package/tui/src/utils/settings/permissionValidation.ts +14 -2
- package/tui/src/utils/shell/prefix.ts +1 -1
- package/tui/src/utils/sideQuery.ts +1 -1
- package/tui/src/utils/systemThemeWatcher.ts +13 -3
- package/tui/src/utils/teleport.tsx +1 -1
- package/uv.lock +426 -45
- package/tui/src/services/api/claude.ts +0 -3540
- package/tui/src/tools/_shared/directPublicDataGuard.ts +0 -362
- package/tui/src/tools/_shared/kmaAnalysisGuard.ts +0 -197
- package/tui/src/tools/_shared/kmaAviationGuard.ts +0 -70
- package/tui/src/tools/_shared/nmcAedGuard.ts +0 -234
- package/tui/src/tools/_shared/protectedCheckGuard.ts +0 -207
- 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
|