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,313 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Document harness evidence records for Evidence Fabric v2.
3
+
4
+ This module stores only join metadata for document reports. Document bytes,
5
+ extracted text, and filled field values stay in the local artifact/report store
6
+ and are referenced here by opaque IDs plus SHA-256 hashes.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+ from pathlib import Path
13
+ from typing import Literal
14
+
15
+ import yaml
16
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
17
+
18
+ from ummaya.evidence.document_authoring_cases import DocumentAuthoringCase
19
+ from ummaya.evidence.json_types import JsonObject, parse_json_object
20
+ from ummaya.evidence.models import RunEvidence
21
+ from ummaya.tools.documents.models import DocumentFormatFamily, KnownDocumentFormat
22
+
23
+ _REPO_ROOT = Path(__file__).resolve().parents[3]
24
+ _DEFAULT_DOCUMENT_SCENARIO_PATH = _REPO_ROOT / "evidence/scenarios/document_harness_v1.yaml"
25
+
26
+ DocumentHarnessReadiness = Literal[
27
+ "ready_for_review",
28
+ "not_ready",
29
+ "blocked",
30
+ "unsupported",
31
+ ]
32
+ DocumentHarnessFormat = Literal["hwpx", "docx", "xlsx", "pdf", "pptx"]
33
+ DocumentHarnessLifecycleStage = Literal[
34
+ "intake",
35
+ "classification",
36
+ "capability",
37
+ "adapter_selection",
38
+ "permission",
39
+ "mutation",
40
+ "render",
41
+ "reread",
42
+ "validation",
43
+ "diff",
44
+ "tui_frame",
45
+ ]
46
+ DocumentHarnessLifecycleStatus = Literal[
47
+ "pass",
48
+ "blocked",
49
+ "needs_input",
50
+ "unsupported",
51
+ "ready_for_review",
52
+ ]
53
+ DocumentHarnessBetaDomain = Literal[
54
+ "weekly_log",
55
+ "contest_proposal",
56
+ "consent",
57
+ "pledge",
58
+ "spreadsheet",
59
+ "pdf_form",
60
+ "presentation",
61
+ "public_data_csv_json",
62
+ "static_pdf",
63
+ "scanned_image",
64
+ "archive_bundle",
65
+ ]
66
+ DocumentHarnessBetaOutcome = Literal[
67
+ "ready_for_review",
68
+ "read_only",
69
+ "blocked",
70
+ "needs_input",
71
+ "unsupported",
72
+ ]
73
+ DocumentHarnessNegativeTrigger = Literal[
74
+ "missing_file",
75
+ "ambiguous_file_candidates",
76
+ "unsupported_known_format",
77
+ "blocked_hwp_write",
78
+ "static_pdf_fill",
79
+ "macro_active_content",
80
+ "path_traversal",
81
+ "oversized_archive",
82
+ "external_link",
83
+ ]
84
+
85
+
86
+ class DocumentHarnessEvidenceError(ValueError):
87
+ """Raised when document harness evidence metadata is invalid."""
88
+
89
+
90
+ class DocumentEvidenceRecord(BaseModel):
91
+ """Joinable evidence record for one generated document derivative."""
92
+
93
+ model_config = ConfigDict(frozen=True, extra="forbid")
94
+
95
+ record_id: str
96
+ scenario_id: str
97
+ correlation_id: str
98
+ source_artifact_id: str
99
+ source_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
100
+ derivative_artifact_id: str
101
+ derivative_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
102
+ structured_diff_id: str
103
+ structured_diff_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
104
+ render_artifact_ids: tuple[str, ...] = Field(min_length=1)
105
+ validation_report_id: str
106
+ validation_report_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
107
+ readiness: DocumentHarnessReadiness
108
+
109
+
110
+ class DocumentHarnessEvidenceEnvelope(BaseModel):
111
+ """Evidence Fabric runner output plus document-specific evidence records."""
112
+
113
+ model_config = ConfigDict(frozen=True, extra="forbid", strict=True)
114
+
115
+ run_evidence: RunEvidence
116
+ document_evidence_records: tuple[DocumentEvidenceRecord, ...]
117
+
118
+
119
+ class DocumentLifecycleEvidenceRecord(BaseModel):
120
+ """Join-only evidence for one document workflow stage."""
121
+
122
+ model_config = ConfigDict(frozen=True, extra="forbid")
123
+
124
+ record_id: str
125
+ scenario_id: str
126
+ correlation_id: str
127
+ stage: DocumentHarnessLifecycleStage
128
+ status: DocumentHarnessLifecycleStatus
129
+ known_format: KnownDocumentFormat
130
+ format_family: DocumentFormatFamily
131
+ support_component_id: str | None = None
132
+ artifact_id: str | None = None
133
+ artifact_sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
134
+ document_diff_id: str | None = None
135
+ frame_hash: str | None = Field(default=None, pattern=r"^sha256:[0-9a-f]{64}$")
136
+ evidence_ref: str
137
+
138
+ @model_validator(mode="after")
139
+ def _tui_frame_requires_frame_hash(self) -> DocumentLifecycleEvidenceRecord:
140
+ if self.stage == "tui_frame" and self.frame_hash is None:
141
+ raise ValueError("tui_frame lifecycle evidence requires frame_hash")
142
+ return self
143
+
144
+
145
+ class DocumentBetaCase(BaseModel):
146
+ """Representative beta scenario for one Public AX document domain."""
147
+
148
+ model_config = ConfigDict(frozen=True, extra="forbid")
149
+
150
+ case_id: str
151
+ domain: DocumentHarnessBetaDomain
152
+ known_format: KnownDocumentFormat
153
+ format_family: DocumentFormatFamily
154
+ expected_outcome: DocumentHarnessBetaOutcome
155
+ expected_operation: str
156
+ fixture_id: str | None = None
157
+ evidence_ref: str
158
+
159
+
160
+ class DocumentNegativeCase(BaseModel):
161
+ """Negative beta scenario that must fail closed."""
162
+
163
+ model_config = ConfigDict(frozen=True, extra="forbid")
164
+
165
+ case_id: str
166
+ trigger: DocumentHarnessNegativeTrigger
167
+ expected_status: Literal["blocked", "needs_input"]
168
+ expected_reason: str
169
+ derivative_save: Literal["forbidden"]
170
+ evidence_ref: str
171
+
172
+
173
+ class DocumentHarnessAcceptanceGates(BaseModel):
174
+ """US4 acceptance gates for the document harness scenario."""
175
+
176
+ model_config = ConfigDict(frozen=True, extra="forbid")
177
+
178
+ original_mutation: Literal["blocked"]
179
+ derivative_lineage: Literal["required"]
180
+ structured_result_schema: Literal["required"]
181
+ render_reread_evidence: Literal["required"]
182
+ live_government_calls: Literal["forbidden"]
183
+
184
+
185
+ class DocumentHarnessFixture(BaseModel):
186
+ """Offline fixture metadata used to prove document evidence joins."""
187
+
188
+ model_config = ConfigDict(frozen=True, extra="forbid")
189
+
190
+ fixture_id: str
191
+ format: DocumentHarnessFormat
192
+ source_artifact_id: str
193
+ source_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
194
+ derivative_artifact_id: str
195
+ derivative_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
196
+ structured_diff_id: str
197
+ structured_diff_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
198
+ render_artifact_ids: tuple[str, ...] = Field(min_length=1)
199
+ validation_report_id: str
200
+ validation_report_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
201
+ expected_correlation_id: str
202
+
203
+
204
+ class DocumentHarnessScenario(BaseModel):
205
+ """Dedicated US4 scenario metadata for document harness evidence."""
206
+
207
+ model_config = ConfigDict(frozen=True, extra="forbid")
208
+
209
+ version: int
210
+ scenario_id: Literal["document_harness_v1"]
211
+ created_at: str
212
+ source_basis: Literal["document_production_hardening_spec"]
213
+ target_system: str
214
+ network_policy: Literal["offline_only"]
215
+ required_sequence: tuple[Literal["document"], ...]
216
+ acceptance_gates: DocumentHarnessAcceptanceGates
217
+ fixtures: tuple[DocumentHarnessFixture, ...] = Field(min_length=1)
218
+ lifecycle_records: tuple[DocumentLifecycleEvidenceRecord, ...] = Field(min_length=1)
219
+ beta_cases: tuple[DocumentBetaCase, ...] = Field(min_length=1)
220
+ negative_cases: tuple[DocumentNegativeCase, ...] = Field(min_length=1)
221
+ authoring_cases: tuple[DocumentAuthoringCase, ...] = Field(min_length=1)
222
+
223
+
224
+ def load_document_harness_scenario(
225
+ path: Path = _DEFAULT_DOCUMENT_SCENARIO_PATH,
226
+ ) -> DocumentHarnessScenario:
227
+ """Load the offline document harness Evidence Fabric scenario."""
228
+
229
+ raw = _load_yaml_mapping(path)
230
+ try:
231
+ return DocumentHarnessScenario.model_validate(raw)
232
+ except ValidationError as exc:
233
+ raise DocumentHarnessEvidenceError(str(exc)) from exc
234
+
235
+
236
+ def attach_document_evidence_records(
237
+ run_evidence: RunEvidence,
238
+ records: Sequence[DocumentEvidenceRecord],
239
+ ) -> DocumentHarnessEvidenceEnvelope:
240
+ """Attach document records to an Evidence Fabric runner result."""
241
+
242
+ return DocumentHarnessEvidenceEnvelope(
243
+ run_evidence=run_evidence,
244
+ document_evidence_records=tuple(records),
245
+ )
246
+
247
+
248
+ def records_from_scenario(
249
+ scenario: DocumentHarnessScenario,
250
+ ) -> tuple[DocumentEvidenceRecord, ...]:
251
+ """Build join-only document evidence records from scenario fixture metadata."""
252
+
253
+ return tuple(
254
+ DocumentEvidenceRecord(
255
+ record_id=f"doc-evidence-{fixture.fixture_id}",
256
+ scenario_id=scenario.scenario_id,
257
+ correlation_id=fixture.expected_correlation_id,
258
+ source_artifact_id=fixture.source_artifact_id,
259
+ source_sha256=fixture.source_sha256,
260
+ derivative_artifact_id=fixture.derivative_artifact_id,
261
+ derivative_sha256=fixture.derivative_sha256,
262
+ structured_diff_id=fixture.structured_diff_id,
263
+ structured_diff_sha256=fixture.structured_diff_sha256,
264
+ render_artifact_ids=fixture.render_artifact_ids,
265
+ validation_report_id=fixture.validation_report_id,
266
+ validation_report_sha256=fixture.validation_report_sha256,
267
+ readiness="ready_for_review",
268
+ )
269
+ for fixture in scenario.fixtures
270
+ )
271
+
272
+
273
+ def lifecycle_records_from_scenario(
274
+ scenario: DocumentHarnessScenario,
275
+ ) -> tuple[DocumentLifecycleEvidenceRecord, ...]:
276
+ """Return workflow lifecycle records from scenario metadata."""
277
+
278
+ return scenario.lifecycle_records
279
+
280
+
281
+ def beta_cases_from_scenario(
282
+ scenario: DocumentHarnessScenario,
283
+ ) -> tuple[DocumentBetaCase, ...]:
284
+ """Return beta-matrix cases from scenario metadata."""
285
+
286
+ return scenario.beta_cases
287
+
288
+
289
+ def negative_cases_from_scenario(
290
+ scenario: DocumentHarnessScenario,
291
+ ) -> tuple[DocumentNegativeCase, ...]:
292
+ """Return fail-closed beta-matrix cases from scenario metadata."""
293
+
294
+ return scenario.negative_cases
295
+
296
+
297
+ def authoring_cases_from_scenario(
298
+ scenario: DocumentHarnessScenario,
299
+ ) -> tuple[DocumentAuthoringCase, ...]:
300
+ return scenario.authoring_cases
301
+
302
+
303
+ def _load_yaml_mapping(path: Path) -> JsonObject:
304
+ resolved = path if path.is_absolute() else _REPO_ROOT / path
305
+ if not resolved.exists():
306
+ raise DocumentHarnessEvidenceError(f"document harness scenario not found: {path}")
307
+ loaded = yaml.safe_load(resolved.read_text(encoding="utf-8"))
308
+ try:
309
+ return parse_json_object(loaded)
310
+ except ValidationError as exc:
311
+ raise DocumentHarnessEvidenceError(
312
+ f"document harness scenario must be a JSON mapping: {path}"
313
+ ) from exc
@@ -0,0 +1,391 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Playwright-captured UX evidence for local document viewers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import zlib
11
+ from collections.abc import Iterator, Mapping
12
+ from contextlib import contextmanager
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from functools import partial
16
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
17
+ from pathlib import Path
18
+ from threading import Thread
19
+ from typing import Literal, Protocol, cast
20
+ from urllib.parse import quote
21
+ from uuid import uuid4
22
+
23
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
24
+
25
+
26
+ class DocumentViewerUxEvidenceError(ValueError):
27
+ """Raised when document viewer UX evidence cannot be captured."""
28
+
29
+
30
+ class DocumentViewerViewportRect(BaseModel):
31
+ """Viewport rectangle from a hidden document viewer manifest."""
32
+
33
+ model_config = ConfigDict(frozen=True, extra="forbid")
34
+
35
+ x: float
36
+ y: float
37
+ width: float
38
+ height: float
39
+
40
+
41
+ class DocumentViewerManifest(BaseModel):
42
+ """Subset of viewer-manifest.json needed for Evidence Fabric joins."""
43
+
44
+ model_config = ConfigDict(frozen=True, extra="allow")
45
+
46
+ viewer_artifact_id: str
47
+ mode: Literal["compact", "expand"]
48
+ page_index: int = 0
49
+ correlation_id: str | None = None
50
+ document_diff_id: str | None = None
51
+ source_render_artifact_id: str
52
+ baseline_render_artifact_id: str | None = None
53
+ viewport_rect: DocumentViewerViewportRect | None = None
54
+ change_ids: tuple[str, ...] = Field(default_factory=tuple)
55
+ local_only: bool = True
56
+
57
+
58
+ class DocumentViewerUxArtifact(BaseModel):
59
+ """Join-only UX artifact record for a Playwright document viewer screenshot."""
60
+
61
+ model_config = ConfigDict(frozen=True, extra="forbid", strict=True)
62
+
63
+ artifact_kind: Literal["document_viewer_png"] = "document_viewer_png"
64
+ artifact_id: str
65
+ source_ref: str
66
+ capture_tool: Literal["playwright"] = "playwright"
67
+ captured_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
68
+ viewer_artifact_id: str
69
+ mode: Literal["compact", "expand"]
70
+ correlation_id: str
71
+ document_diff_id: str | None = None
72
+ viewer_html_path: str
73
+ viewer_manifest_path: str
74
+ screenshot_path: str
75
+ screenshot_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
76
+ frame_hash: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
77
+ page_index: int
78
+ source_render_artifact_id: str
79
+ baseline_render_artifact_id: str | None = None
80
+ viewport_rect: DocumentViewerViewportRect | None = None
81
+ change_ids: tuple[str, ...] = Field(default_factory=tuple)
82
+ local_only: bool
83
+
84
+
85
+ class PlaywrightCapture(Protocol):
86
+ """Callable boundary for Playwright screenshot capture."""
87
+
88
+ def __call__(self, *, viewer_url: str, output_path: Path) -> None:
89
+ """Capture ``viewer_url`` into ``output_path`` as a PNG."""
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class _PngImageData:
94
+ width: int
95
+ height: int
96
+ color_type: int
97
+ compressed_data: bytes
98
+
99
+
100
+ def capture_document_viewer_ux_artifact(
101
+ *,
102
+ viewer_html_path: Path,
103
+ output_dir: Path,
104
+ source_ref: str,
105
+ correlation_id: str | None = None,
106
+ document_diff_id: str | None = None,
107
+ capture: PlaywrightCapture | None = None,
108
+ ) -> DocumentViewerUxArtifact:
109
+ """Capture a local document viewer with Playwright and return join metadata."""
110
+
111
+ viewer_html = viewer_html_path.resolve()
112
+ if not viewer_html.exists():
113
+ raise DocumentViewerUxEvidenceError(f"document viewer html not found: {viewer_html}")
114
+ manifest_path = viewer_html.parent / "viewer-manifest.json"
115
+ manifest = _load_manifest(manifest_path)
116
+ resolved_correlation_id = correlation_id or manifest.correlation_id
117
+ if resolved_correlation_id is None or resolved_correlation_id.strip() == "":
118
+ raise DocumentViewerUxEvidenceError(
119
+ "document viewer UX evidence requires a correlation_id in the "
120
+ "viewer manifest or CLI argument"
121
+ )
122
+ resolved_diff_id = document_diff_id or manifest.document_diff_id
123
+
124
+ output_dir.mkdir(parents=True, exist_ok=True)
125
+ screenshot_path = (
126
+ output_dir / f"{manifest.viewer_artifact_id}-{manifest.mode}-playwright.png"
127
+ ).resolve()
128
+ capture_impl = capture or capture_with_playwright_cli
129
+ with _serve_viewer_directory(viewer_html) as viewer_url:
130
+ capture_impl(viewer_url=viewer_url, output_path=screenshot_path)
131
+ if not screenshot_path.exists():
132
+ raise DocumentViewerUxEvidenceError(
133
+ f"Playwright did not create screenshot: {screenshot_path}"
134
+ )
135
+ if not _png_has_visible_nonwhite_pixel(screenshot_path):
136
+ raise DocumentViewerUxEvidenceError(
137
+ f"Playwright document viewer capture is blank: {screenshot_path}"
138
+ )
139
+ screenshot_sha256 = _sha256_file(screenshot_path)
140
+
141
+ return DocumentViewerUxArtifact(
142
+ artifact_id=f"ux-{manifest.viewer_artifact_id}-{screenshot_sha256[:12]}",
143
+ source_ref=source_ref,
144
+ viewer_artifact_id=manifest.viewer_artifact_id,
145
+ mode=manifest.mode,
146
+ correlation_id=resolved_correlation_id,
147
+ document_diff_id=resolved_diff_id,
148
+ viewer_html_path=str(viewer_html),
149
+ viewer_manifest_path=str(manifest_path.resolve()),
150
+ screenshot_path=str(screenshot_path),
151
+ screenshot_sha256=screenshot_sha256,
152
+ frame_hash=f"sha256:{screenshot_sha256}",
153
+ page_index=manifest.page_index,
154
+ source_render_artifact_id=manifest.source_render_artifact_id,
155
+ baseline_render_artifact_id=manifest.baseline_render_artifact_id,
156
+ viewport_rect=manifest.viewport_rect,
157
+ change_ids=manifest.change_ids,
158
+ local_only=manifest.local_only,
159
+ )
160
+
161
+
162
+ def capture_with_playwright_cli(*, viewer_url: str, output_path: Path) -> None:
163
+ """Capture a viewer URL through the bundled Playwright CLI wrapper."""
164
+
165
+ wrapper = Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex"))) / (
166
+ "skills/playwright/scripts/playwright_cli.sh"
167
+ )
168
+ if not wrapper.exists():
169
+ raise DocumentViewerUxEvidenceError(f"Playwright CLI wrapper not found: {wrapper}")
170
+ session = f"udv-{uuid4().hex[:12]}"
171
+ env = os.environ.copy()
172
+ output_path.parent.mkdir(parents=True, exist_ok=True)
173
+ commands = (
174
+ (str(wrapper), f"-s={session}", "open", viewer_url),
175
+ (str(wrapper), f"-s={session}", "resize", "1600", "1000"),
176
+ (
177
+ str(wrapper),
178
+ f"-s={session}",
179
+ "eval",
180
+ "() => document.documentElement.dataset.ready || document.readyState",
181
+ ),
182
+ (
183
+ str(wrapper),
184
+ f"-s={session}",
185
+ "screenshot",
186
+ "--filename",
187
+ str(output_path),
188
+ "--full-page",
189
+ ),
190
+ )
191
+ try:
192
+ for command in commands:
193
+ subprocess.run( # noqa: S603 - fixed local Playwright wrapper plus typed args.
194
+ command,
195
+ check=True,
196
+ capture_output=True,
197
+ text=True,
198
+ env=env,
199
+ )
200
+ except subprocess.CalledProcessError as exc:
201
+ detail = (exc.stderr or exc.stdout or str(exc)).strip()
202
+ raise DocumentViewerUxEvidenceError(
203
+ f"Playwright document viewer capture failed: {detail}"
204
+ ) from exc
205
+ finally:
206
+ subprocess.run( # noqa: S603 - cleanup uses the same fixed local wrapper.
207
+ (str(wrapper), f"-s={session}", "close"),
208
+ check=False,
209
+ capture_output=True,
210
+ text=True,
211
+ env=env,
212
+ )
213
+
214
+
215
+ def _load_manifest(path: Path) -> DocumentViewerManifest:
216
+ if not path.exists():
217
+ raise DocumentViewerUxEvidenceError(f"viewer manifest not found: {path}")
218
+ loaded = json.loads(path.read_text(encoding="utf-8"))
219
+ if not isinstance(loaded, Mapping):
220
+ raise DocumentViewerUxEvidenceError(f"viewer manifest must be a mapping: {path}")
221
+ try:
222
+ return DocumentViewerManifest.model_validate(cast(Mapping[str, object], loaded))
223
+ except ValidationError as exc:
224
+ raise DocumentViewerUxEvidenceError(str(exc)) from exc
225
+
226
+
227
+ @contextmanager
228
+ def _serve_viewer_directory(viewer_html: Path) -> Iterator[str]:
229
+ handler = partial(
230
+ _QuietViewerRequestHandler,
231
+ directory=str(viewer_html.parent),
232
+ )
233
+ server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
234
+ thread = Thread(target=server.serve_forever, daemon=True)
235
+ thread.start()
236
+ try:
237
+ port = int(server.server_address[1])
238
+ yield f"http://127.0.0.1:{port}/{quote(viewer_html.name)}"
239
+ finally:
240
+ server.shutdown()
241
+ server.server_close()
242
+ thread.join(timeout=2)
243
+
244
+
245
+ class _QuietViewerRequestHandler(SimpleHTTPRequestHandler):
246
+ def log_message(self, message_format: str, *args: object) -> None:
247
+ del message_format, args
248
+
249
+
250
+ def _png_has_visible_nonwhite_pixel(path: Path) -> bool:
251
+ image = _load_png_image_data(path)
252
+ channels = 4 if image.color_type == 6 else 3
253
+ stride = image.width * channels
254
+ raw = zlib.decompress(image.compressed_data)
255
+ previous = bytearray(stride)
256
+ offset = 0
257
+ for _row in range(image.height):
258
+ filter_type, scanline, offset = _read_png_scanline(
259
+ raw,
260
+ offset,
261
+ stride,
262
+ path,
263
+ )
264
+ _unfilter_png_scanline(scanline, previous, channels, filter_type)
265
+ if _scanline_has_visible_nonwhite_pixel(scanline, channels):
266
+ return True
267
+ previous = scanline
268
+ return False
269
+
270
+
271
+ def _load_png_image_data(path: Path) -> _PngImageData:
272
+ data = path.read_bytes()
273
+ if not data.startswith(b"\x89PNG\r\n\x1a\n"):
274
+ raise DocumentViewerUxEvidenceError(f"Playwright screenshot is not a PNG: {path}")
275
+ width = 0
276
+ height = 0
277
+ bit_depth = 0
278
+ color_type = 0
279
+ interlace = 0
280
+ idat_chunks: list[bytes] = []
281
+ for chunk_type, chunk_data in _iter_png_chunks(data, path):
282
+ if chunk_type == b"IHDR":
283
+ width = int.from_bytes(chunk_data[0:4], "big")
284
+ height = int.from_bytes(chunk_data[4:8], "big")
285
+ bit_depth = chunk_data[8]
286
+ color_type = chunk_data[9]
287
+ interlace = chunk_data[12]
288
+ elif chunk_type == b"IDAT":
289
+ idat_chunks.append(chunk_data)
290
+ elif chunk_type == b"IEND":
291
+ break
292
+ if width <= 0 or height <= 0 or not idat_chunks:
293
+ raise DocumentViewerUxEvidenceError(f"invalid PNG screenshot: {path}")
294
+ if bit_depth != 8 or color_type not in (2, 6) or interlace != 0:
295
+ raise DocumentViewerUxEvidenceError(
296
+ f"unsupported PNG screenshot format: bit_depth={bit_depth} "
297
+ f"color_type={color_type} interlace={interlace}"
298
+ )
299
+ return _PngImageData(
300
+ width=width,
301
+ height=height,
302
+ color_type=color_type,
303
+ compressed_data=b"".join(idat_chunks),
304
+ )
305
+
306
+
307
+ def _iter_png_chunks(data: bytes, path: Path) -> Iterator[tuple[bytes, bytes]]:
308
+ pos = 8
309
+ while pos + 8 <= len(data):
310
+ chunk_len = int.from_bytes(data[pos : pos + 4], "big")
311
+ chunk_type = data[pos + 4 : pos + 8]
312
+ chunk_start = pos + 8
313
+ chunk_end = chunk_start + chunk_len
314
+ if chunk_end + 4 > len(data):
315
+ raise DocumentViewerUxEvidenceError(f"invalid PNG chunk bounds: {path}")
316
+ yield chunk_type, data[chunk_start:chunk_end]
317
+ pos = chunk_end + 4
318
+ if chunk_type == b"IEND":
319
+ break
320
+
321
+
322
+ def _read_png_scanline(
323
+ raw: bytes,
324
+ offset: int,
325
+ stride: int,
326
+ path: Path,
327
+ ) -> tuple[int, bytearray, int]:
328
+ if offset >= len(raw):
329
+ raise DocumentViewerUxEvidenceError(f"truncated PNG scanline data: {path}")
330
+ filter_type = raw[offset]
331
+ next_offset = offset + 1
332
+ scanline = bytearray(raw[next_offset : next_offset + stride])
333
+ if len(scanline) != stride:
334
+ raise DocumentViewerUxEvidenceError(f"truncated PNG scanline data: {path}")
335
+ return filter_type, scanline, next_offset + stride
336
+
337
+
338
+ def _scanline_has_visible_nonwhite_pixel(scanline: bytearray, channels: int) -> bool:
339
+ for pixel in range(0, len(scanline), channels):
340
+ red = scanline[pixel]
341
+ green = scanline[pixel + 1]
342
+ blue = scanline[pixel + 2]
343
+ alpha = scanline[pixel + 3] if channels == 4 else 255
344
+ if alpha > 0 and (red < 250 or green < 250 or blue < 250):
345
+ return True
346
+ return False
347
+
348
+
349
+ def _unfilter_png_scanline(
350
+ scanline: bytearray,
351
+ previous: bytearray,
352
+ bytes_per_pixel: int,
353
+ filter_type: int,
354
+ ) -> None:
355
+ for index, value in enumerate(scanline):
356
+ left = scanline[index - bytes_per_pixel] if index >= bytes_per_pixel else 0
357
+ up = previous[index]
358
+ up_left = previous[index - bytes_per_pixel] if index >= bytes_per_pixel else 0
359
+ if filter_type == 0:
360
+ restored = value
361
+ elif filter_type == 1:
362
+ restored = value + left
363
+ elif filter_type == 2:
364
+ restored = value + up
365
+ elif filter_type == 3:
366
+ restored = value + ((left + up) // 2)
367
+ elif filter_type == 4:
368
+ restored = value + _paeth_predictor(left, up, up_left)
369
+ else:
370
+ raise DocumentViewerUxEvidenceError(f"unsupported PNG filter type: {filter_type}")
371
+ scanline[index] = restored & 0xFF
372
+
373
+
374
+ def _paeth_predictor(left: int, up: int, up_left: int) -> int:
375
+ estimate = left + up - up_left
376
+ distance_left = abs(estimate - left)
377
+ distance_up = abs(estimate - up)
378
+ distance_up_left = abs(estimate - up_left)
379
+ if distance_left <= distance_up and distance_left <= distance_up_left:
380
+ return left
381
+ if distance_up <= distance_up_left:
382
+ return up
383
+ return up_left
384
+
385
+
386
+ def _sha256_file(path: Path) -> str:
387
+ digest = hashlib.sha256()
388
+ with path.open("rb") as file:
389
+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
390
+ digest.update(chunk)
391
+ return digest.hexdigest()