redmine-context 1.0.0

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 (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Extrator de texto de documentos OOXML — docx/pptx/xlsx (#184, ADR-002/ADR-005).
3
+ *
4
+ * 100% LOCAL e SEM BINÁRIO externo (não entra no `doctor`; funciona nos 3 SOs sem
5
+ * instalar nada): um arquivo Office moderno é um contêiner ZIP com o conteúdo em
6
+ * XML. Este extrator abre o zip ({@link parseZip}, zero-dep), DESAMBIGUA o formato
7
+ * pelo conteúdo (não pela extensão nem pelo Content-Type) e puxa o texto das tags
8
+ * de run de cada dialeto:
9
+ *
10
+ * docx → `word/document.xml` tags `<w:t>` (parágrafo `</w:p>`, tab `<w:tab/>`)
11
+ * pptx → `ppt/slides/slide<N>.xml` tags `<a:t>` (parágrafo `</a:p>`), na ordem dos slides
12
+ * xlsx → `xl/sharedStrings.xml` tags `<t>` (uma string por `</si>`)
13
+ *
14
+ * É registrado para o MIME `application/zip` (o `magic.ts` reporta OOXML como zip;
15
+ * a "diferenciação fina" fica aqui, como o próprio `magic.ts` documenta). Um zip
16
+ * que NÃO é OOXML devolve `unsupported`; zip corrompido/entrada ausente/vazio
17
+ * devolve `failed` — degradação graciosa, NUNCA crash (ADR-002).
18
+ *
19
+ * ROBUSTEZ CONTRA INPUT ADVERSARIAL (anexos são input não confiável de qualquer
20
+ * usuário do Redmine):
21
+ * - a extração de texto é um SCANNER DE UM PASSO (O(n) linear, sem backtracking) —
22
+ * NÃO usa `regex.matchAll` sobre o documento inteiro, que degeneraria para O(n²)
23
+ * com tags de texto abertas e nunca fechadas (DoS de CPU trivial);
24
+ * - orçamento AGREGADO de descompressão no pptx (não só por-entrada) + teto de
25
+ * slides, contra zip-bombs de muitas entradas;
26
+ * - `unescapeXml` valida o range Unicode antes de `fromCodePoint` (entidade
27
+ * numérica fora de range é mantida literal, nunca lança).
28
+ *
29
+ * O texto extraído é marcado como não-confiável no bundle pelo mesmo mecanismo do
30
+ * tesseract/pdf (`<untrusted-content>`), então aqui só devolvemos `{ status:'done', text }`.
31
+ */
32
+ import type { ExtractorParams } from '../cache/contract.js';
33
+ import type { ExtractorConfig } from '../cache/keys.js';
34
+ import type { ExtractionResult } from '../contract.js';
35
+ import { type ExtractOptions, type Extractor } from './dispatcher.js';
36
+ /** MIME REAL (magic bytes `PK\x03\x04`) que roteia este extrator. */
37
+ export declare const OOXML_MIMES: readonly string[];
38
+ /**
39
+ * Desescapa entidades XML (nomeadas + numéricas decimais/hex). Sequências
40
+ * desconhecidas OU com code point fora do range Unicode são mantidas LITERAIS —
41
+ * `String.fromCodePoint` nunca é chamado com valor inválido (não lança).
42
+ *
43
+ * @param s - Texto com entidades XML.
44
+ * @returns Texto com as entidades resolvidas.
45
+ * @internal Exportado para teste unitário (range de entidade numérica).
46
+ */
47
+ export declare function unescapeXml(s: string): string;
48
+ /** Dialeto de um formato: tag de texto + tags que viram quebra de linha/tabulação. */
49
+ interface Dialect {
50
+ /** Nome da tag de run de texto (ex.: `w:t`, `a:t`, `t`). */
51
+ readonly textTag: string;
52
+ /** Tags de FECHAMENTO que marcam fim de parágrafo/registro → `\n` (ex.: `w:p`, `si`). */
53
+ readonly newlineClose: ReadonlySet<string>;
54
+ /** Tags de ABERTURA (geralmente self-close) que viram `\n` (ex.: `w:br`, `w:cr`). */
55
+ readonly newlineOpen: ReadonlySet<string>;
56
+ /** Tags de ABERTURA que viram tabulação (ex.: `w:tab`). */
57
+ readonly tabOpen: ReadonlySet<string>;
58
+ }
59
+ /** @internal Exportado para teste unitário (guarda de regressão de ReDoS). */
60
+ export declare const DOCX_DIALECT: Dialect;
61
+ /**
62
+ * Extrai texto de um XML OOXML com um SCANNER DE UM PASSO (O(n), sem backtracking).
63
+ * Percorre o buffer uma vez: em cada tag de TEXTO de run do dialeto captura o
64
+ * conteúdo até o próximo `<` (runs OOXML não têm filhos), e converte as tags
65
+ * estruturais (parágrafo/break → `\n`, tab → `\t`). Todo o resto — inclusive o
66
+ * espaço em branco insignificante entre elementos — é ignorado.
67
+ *
68
+ * @param xml - Conteúdo XML da parte.
69
+ * @param d - Dialeto do formato.
70
+ * @returns Texto concatenado com quebras/tabs nos limites estruturais.
71
+ */
72
+ export declare function extractTextFromXml(xml: string, d: Dialect): string;
73
+ /**
74
+ * Extrator OOXML (docx/pptx/xlsx). Implementa {@link Extractor} e expõe
75
+ * {@link params}/{@link extractorConfig} estáveis para a chave de cache (ADR-004).
76
+ */
77
+ export declare class OoxmlExtractor implements Extractor {
78
+ readonly id = "ooxml";
79
+ readonly version = "ooxml-1";
80
+ readonly supportedMimes: readonly string[];
81
+ readonly model = "ooxml";
82
+ /** Sem parâmetros escalares que alterem a saída (extração determinística). */
83
+ readonly params: ExtractorParams;
84
+ /**
85
+ * Configuração pronta para {@link buildAttachmentKey} (ADR-004).
86
+ * @returns `{ version, model, params }` estáveis desta instância.
87
+ */
88
+ get extractorConfig(): ExtractorConfig;
89
+ /**
90
+ * Extrai o texto de um arquivo OOXML. Nunca lança: zip inválido/entrada
91
+ * ausente/vazio → `failed`; zip que não é OOXML → `unsupported`; signal já
92
+ * abortado → `cancelled`.
93
+ *
94
+ * @param filePath - Caminho absoluto do arquivo baixado.
95
+ * @param options - MIME real + logger + signal — ver {@link ExtractOptions}.
96
+ * @returns `done` com `text` no sucesso; `unsupported`/`failed`/`cancelled` caso contrário.
97
+ */
98
+ extract(filePath: string, options: ExtractOptions): Promise<ExtractionResult>;
99
+ /**
100
+ * Concatena o texto de todos os slides do pptx, na ORDEM numérica dos slides,
101
+ * com orçamento AGREGADO de bytes descomprimidos e teto de nº de slides.
102
+ *
103
+ * @param zip - Arquivo OOXML aberto.
104
+ * @param logger - Logger opcional para avisos (ex.: teto de slides atingido).
105
+ * @returns Texto concatenado dos slides.
106
+ * @throws {Error} Se o total descomprimido exceder {@link MAX_TOTAL_BYTES}.
107
+ */
108
+ private extractPptx;
109
+ /**
110
+ * Monta um resultado terminal simples (`unsupported`/`cancelled`) com metadados.
111
+ * @param status - Status terminal.
112
+ * @param mime - MIME real detectado.
113
+ * @param extra - Metadados adicionais.
114
+ */
115
+ private terminal;
116
+ /**
117
+ * Monta um `ExtractionResult` `failed` com diagnóstico (sem chaves `undefined`).
118
+ * @param mime - MIME real detectado.
119
+ * @param reason - Motivo canônico da falha.
120
+ * @param extra - Metadados adicionais (erro/hint/format).
121
+ */
122
+ private failed;
123
+ }
124
+ /**
125
+ * Cria um {@link OoxmlExtractor} pronto para registro. Sem trabalho assíncrono
126
+ * (não há binário a localizar) — a fábrica existe por simetria com os demais.
127
+ *
128
+ * @returns Uma instância do extrator OOXML.
129
+ */
130
+ export declare function createOoxmlExtractor(): OoxmlExtractor;
131
+ export {};
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Extrator de texto de documentos OOXML — docx/pptx/xlsx (#184, ADR-002/ADR-005).
3
+ *
4
+ * 100% LOCAL e SEM BINÁRIO externo (não entra no `doctor`; funciona nos 3 SOs sem
5
+ * instalar nada): um arquivo Office moderno é um contêiner ZIP com o conteúdo em
6
+ * XML. Este extrator abre o zip ({@link parseZip}, zero-dep), DESAMBIGUA o formato
7
+ * pelo conteúdo (não pela extensão nem pelo Content-Type) e puxa o texto das tags
8
+ * de run de cada dialeto:
9
+ *
10
+ * docx → `word/document.xml` tags `<w:t>` (parágrafo `</w:p>`, tab `<w:tab/>`)
11
+ * pptx → `ppt/slides/slide<N>.xml` tags `<a:t>` (parágrafo `</a:p>`), na ordem dos slides
12
+ * xlsx → `xl/sharedStrings.xml` tags `<t>` (uma string por `</si>`)
13
+ *
14
+ * É registrado para o MIME `application/zip` (o `magic.ts` reporta OOXML como zip;
15
+ * a "diferenciação fina" fica aqui, como o próprio `magic.ts` documenta). Um zip
16
+ * que NÃO é OOXML devolve `unsupported`; zip corrompido/entrada ausente/vazio
17
+ * devolve `failed` — degradação graciosa, NUNCA crash (ADR-002).
18
+ *
19
+ * ROBUSTEZ CONTRA INPUT ADVERSARIAL (anexos são input não confiável de qualquer
20
+ * usuário do Redmine):
21
+ * - a extração de texto é um SCANNER DE UM PASSO (O(n) linear, sem backtracking) —
22
+ * NÃO usa `regex.matchAll` sobre o documento inteiro, que degeneraria para O(n²)
23
+ * com tags de texto abertas e nunca fechadas (DoS de CPU trivial);
24
+ * - orçamento AGREGADO de descompressão no pptx (não só por-entrada) + teto de
25
+ * slides, contra zip-bombs de muitas entradas;
26
+ * - `unescapeXml` valida o range Unicode antes de `fromCodePoint` (entidade
27
+ * numérica fora de range é mantida literal, nunca lança).
28
+ *
29
+ * O texto extraído é marcado como não-confiável no bundle pelo mesmo mecanismo do
30
+ * tesseract/pdf (`<untrusted-content>`), então aqui só devolvemos `{ status:'done', text }`.
31
+ */
32
+ import { readFile, stat } from 'node:fs/promises';
33
+ import { parseZip } from './zip.js';
34
+ /** Identificador estável do extrator (entra em metadados/cache-key). */
35
+ const EXTRACTOR_ID = 'ooxml';
36
+ /** Modelo lógico para a chave de cache (ADR-004). */
37
+ const EXTRACTOR_MODEL = 'ooxml';
38
+ /** Versão da integração (participa da chave de cache; bump ao mudar a extração). */
39
+ const EXTRACTOR_VERSION = 'ooxml-1';
40
+ /** Teto do arquivo (verificado por `stat` ANTES do `readFile`; download já limita). */
41
+ const MAX_FILE_BYTES = 100 * 1024 * 1024;
42
+ /**
43
+ * Teto AGREGADO de texto descomprimido lido numa única extração de pptx (soma de
44
+ * todos os slides). Complementa o teto por-entrada de {@link parseZip}: impede que
45
+ * um `.pptx` pequeno com muitas entradas altamente compressíveis exploda em RAM/CPU.
46
+ */
47
+ const MAX_TOTAL_BYTES = 128 * 1024 * 1024;
48
+ /** Teto de slides processados num pptx (defesa contra "milhares de slides vazios"). */
49
+ const MAX_SLIDES = 5000;
50
+ /** MIME REAL (magic bytes `PK\x03\x04`) que roteia este extrator. */
51
+ export const OOXML_MIMES = ['application/zip'];
52
+ /** Entradas-âncora que identificam cada formato OOXML dentro do zip. */
53
+ const DOCX_MAIN = 'word/document.xml';
54
+ const PPTX_MAIN = 'ppt/presentation.xml';
55
+ const XLSX_MAIN = 'xl/workbook.xml';
56
+ const XLSX_STRINGS = 'xl/sharedStrings.xml';
57
+ /** Regex de um nome de slide do pptx, capturando o número para ordenação. */
58
+ const SLIDE_RE = /^ppt\/slides\/slide(\d+)\.xml$/;
59
+ /** Entidades XML nomeadas suportadas no desescape. */
60
+ const NAMED_ENTITIES = {
61
+ amp: '&',
62
+ lt: '<',
63
+ gt: '>',
64
+ quot: '"',
65
+ apos: "'",
66
+ };
67
+ /** Maior code point Unicode válido (limite de `String.fromCodePoint`). */
68
+ const MAX_CODE_POINT = 0x10ffff;
69
+ /**
70
+ * Desescapa entidades XML (nomeadas + numéricas decimais/hex). Sequências
71
+ * desconhecidas OU com code point fora do range Unicode são mantidas LITERAIS —
72
+ * `String.fromCodePoint` nunca é chamado com valor inválido (não lança).
73
+ *
74
+ * @param s - Texto com entidades XML.
75
+ * @returns Texto com as entidades resolvidas.
76
+ * @internal Exportado para teste unitário (range de entidade numérica).
77
+ */
78
+ export function unescapeXml(s) {
79
+ return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, code) => {
80
+ if (code.startsWith('#')) {
81
+ const hex = code.startsWith('#x') || code.startsWith('#X');
82
+ const cp = Number.parseInt(hex ? code.slice(2) : code.slice(1), hex ? 16 : 10);
83
+ return Number.isInteger(cp) && cp >= 0 && cp <= MAX_CODE_POINT
84
+ ? String.fromCodePoint(cp)
85
+ : match;
86
+ }
87
+ return NAMED_ENTITIES[code] ?? match;
88
+ });
89
+ }
90
+ /** Nome + tipo de uma tag a partir do seu interior (entre `<` e `>`). Bounded/linear. */
91
+ const TAG_RE = /^\s*(\/?)\s*([^\s/>]+)/;
92
+ function parseTag(inner) {
93
+ const selfClose = inner.endsWith('/');
94
+ const m = TAG_RE.exec(inner);
95
+ return { name: m?.[2] ?? '', closing: m?.[1] === '/', selfClose };
96
+ }
97
+ /** @internal Exportado para teste unitário (guarda de regressão de ReDoS). */
98
+ export const DOCX_DIALECT = {
99
+ textTag: 'w:t',
100
+ newlineClose: new Set(['w:p']),
101
+ newlineOpen: new Set(['w:br', 'w:cr']),
102
+ tabOpen: new Set(['w:tab']),
103
+ };
104
+ const PPTX_DIALECT = {
105
+ textTag: 'a:t',
106
+ newlineClose: new Set(['a:p']),
107
+ newlineOpen: new Set(['a:br']),
108
+ tabOpen: new Set(),
109
+ };
110
+ const XLSX_DIALECT = {
111
+ textTag: 't',
112
+ newlineClose: new Set(['si']),
113
+ newlineOpen: new Set(),
114
+ tabOpen: new Set(),
115
+ };
116
+ /**
117
+ * Extrai texto de um XML OOXML com um SCANNER DE UM PASSO (O(n), sem backtracking).
118
+ * Percorre o buffer uma vez: em cada tag de TEXTO de run do dialeto captura o
119
+ * conteúdo até o próximo `<` (runs OOXML não têm filhos), e converte as tags
120
+ * estruturais (parágrafo/break → `\n`, tab → `\t`). Todo o resto — inclusive o
121
+ * espaço em branco insignificante entre elementos — é ignorado.
122
+ *
123
+ * @param xml - Conteúdo XML da parte.
124
+ * @param d - Dialeto do formato.
125
+ * @returns Texto concatenado com quebras/tabs nos limites estruturais.
126
+ */
127
+ export function extractTextFromXml(xml, d) {
128
+ let out = '';
129
+ const n = xml.length;
130
+ let i = 0;
131
+ while (i < n) {
132
+ const lt = xml.indexOf('<', i);
133
+ if (lt < 0)
134
+ break;
135
+ const gt = xml.indexOf('>', lt + 1);
136
+ if (gt < 0)
137
+ break; // tag não terminada (cauda malformada) — para com segurança
138
+ const tag = parseTag(xml.slice(lt + 1, gt));
139
+ i = gt + 1;
140
+ if (!tag.closing && !tag.selfClose && tag.name === d.textTag) {
141
+ // Conteúdo do run: do fim desta tag até o próximo `<` (o `</w:t>`). `<` só
142
+ // aparece escapado como `&lt;` dentro do texto, então isto é seguro.
143
+ const nextLt = xml.indexOf('<', i);
144
+ const end = nextLt < 0 ? n : nextLt;
145
+ out += unescapeXml(xml.slice(i, end));
146
+ i = end;
147
+ }
148
+ else if (tag.closing && d.newlineClose.has(tag.name)) {
149
+ out += '\n';
150
+ }
151
+ else if (!tag.closing && d.newlineOpen.has(tag.name)) {
152
+ out += '\n';
153
+ }
154
+ else if (!tag.closing && d.tabOpen.has(tag.name)) {
155
+ out += '\t';
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+ /** Normaliza o texto extraído: tira espaço antes de `\n` e colapsa 3+ quebras. */
161
+ function normalize(text) {
162
+ return text.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
163
+ }
164
+ /**
165
+ * Extrator OOXML (docx/pptx/xlsx). Implementa {@link Extractor} e expõe
166
+ * {@link params}/{@link extractorConfig} estáveis para a chave de cache (ADR-004).
167
+ */
168
+ export class OoxmlExtractor {
169
+ id = EXTRACTOR_ID;
170
+ version = EXTRACTOR_VERSION;
171
+ supportedMimes = OOXML_MIMES;
172
+ model = EXTRACTOR_MODEL;
173
+ /** Sem parâmetros escalares que alterem a saída (extração determinística). */
174
+ params = {};
175
+ /**
176
+ * Configuração pronta para {@link buildAttachmentKey} (ADR-004).
177
+ * @returns `{ version, model, params }` estáveis desta instância.
178
+ */
179
+ get extractorConfig() {
180
+ return { version: this.version, model: this.model, params: this.params };
181
+ }
182
+ /**
183
+ * Extrai o texto de um arquivo OOXML. Nunca lança: zip inválido/entrada
184
+ * ausente/vazio → `failed`; zip que não é OOXML → `unsupported`; signal já
185
+ * abortado → `cancelled`.
186
+ *
187
+ * @param filePath - Caminho absoluto do arquivo baixado.
188
+ * @param options - MIME real + logger + signal — ver {@link ExtractOptions}.
189
+ * @returns `done` com `text` no sucesso; `unsupported`/`failed`/`cancelled` caso contrário.
190
+ */
191
+ async extract(filePath, options) {
192
+ if (options.signal?.aborted) {
193
+ return this.terminal('cancelled', options.mime, { reason: 'cancelado' });
194
+ }
195
+ // Checa o tamanho ANTES de carregar o arquivo na memória (defesa real caso o
196
+ // cap de download upstream falhe/seja contornado).
197
+ let size;
198
+ try {
199
+ size = (await stat(filePath)).size;
200
+ }
201
+ catch (error) {
202
+ return this.failed(options.mime, 'erro-leitura', { error: message(error) });
203
+ }
204
+ if (size > MAX_FILE_BYTES) {
205
+ return this.failed(options.mime, 'arquivo-muito-grande', { bytes: size });
206
+ }
207
+ let buffer;
208
+ try {
209
+ buffer = await readFile(filePath);
210
+ }
211
+ catch (error) {
212
+ return this.failed(options.mime, 'erro-leitura', { error: message(error) });
213
+ }
214
+ let zip;
215
+ try {
216
+ zip = parseZip(buffer);
217
+ }
218
+ catch (error) {
219
+ return this.failed(options.mime, 'zip-invalido', { error: message(error) });
220
+ }
221
+ let format;
222
+ let text;
223
+ try {
224
+ if (zip.has(DOCX_MAIN)) {
225
+ format = 'docx';
226
+ text = extractTextFromXml(readText(zip, DOCX_MAIN), DOCX_DIALECT);
227
+ }
228
+ else if (zip.has(PPTX_MAIN)) {
229
+ format = 'pptx';
230
+ text = this.extractPptx(zip, options.logger);
231
+ }
232
+ else if (zip.has(XLSX_MAIN)) {
233
+ format = 'xlsx';
234
+ text = zip.has(XLSX_STRINGS)
235
+ ? extractTextFromXml(readText(zip, XLSX_STRINGS), XLSX_DIALECT)
236
+ : '';
237
+ }
238
+ else {
239
+ return this.terminal('unsupported', options.mime, { reason: 'zip-nao-ooxml' });
240
+ }
241
+ }
242
+ catch (error) {
243
+ return this.failed(options.mime, 'erro-extracao', { error: message(error) });
244
+ }
245
+ const normalized = normalize(text);
246
+ if (normalized.length === 0) {
247
+ return this.failed(options.mime, 'ooxml-sem-texto', {
248
+ format,
249
+ hint: 'documento OOXML sem texto extraível (ex.: planilha só com números ou slides só com imagens)',
250
+ });
251
+ }
252
+ return {
253
+ status: 'done',
254
+ text: normalized,
255
+ mime: options.mime,
256
+ metadata: { extractorId: this.id, version: this.version, format },
257
+ };
258
+ }
259
+ /**
260
+ * Concatena o texto de todos os slides do pptx, na ORDEM numérica dos slides,
261
+ * com orçamento AGREGADO de bytes descomprimidos e teto de nº de slides.
262
+ *
263
+ * @param zip - Arquivo OOXML aberto.
264
+ * @param logger - Logger opcional para avisos (ex.: teto de slides atingido).
265
+ * @returns Texto concatenado dos slides.
266
+ * @throws {Error} Se o total descomprimido exceder {@link MAX_TOTAL_BYTES}.
267
+ */
268
+ extractPptx(zip, logger) {
269
+ const slides = zip
270
+ .names()
271
+ .map((name) => {
272
+ const m = SLIDE_RE.exec(name);
273
+ return m ? { name, n: Number.parseInt(m[1], 10) } : undefined;
274
+ })
275
+ .filter((x) => x !== undefined)
276
+ .sort((a, b) => a.n - b.n);
277
+ if (slides.length > MAX_SLIDES) {
278
+ logger?.warn(`ooxml: pptx com ${slides.length} slides; processando apenas os primeiros ${MAX_SLIDES}`);
279
+ }
280
+ const capped = slides.slice(0, MAX_SLIDES);
281
+ let total = 0;
282
+ let out = '';
283
+ for (const slide of capped) {
284
+ const xml = readText(zip, slide.name);
285
+ total += Buffer.byteLength(xml, 'utf8');
286
+ if (total > MAX_TOTAL_BYTES) {
287
+ throw new Error(`orçamento de descompressão do pptx excedido (${MAX_TOTAL_BYTES} bytes)`);
288
+ }
289
+ out += extractTextFromXml(xml, PPTX_DIALECT);
290
+ }
291
+ return out;
292
+ }
293
+ /**
294
+ * Monta um resultado terminal simples (`unsupported`/`cancelled`) com metadados.
295
+ * @param status - Status terminal.
296
+ * @param mime - MIME real detectado.
297
+ * @param extra - Metadados adicionais.
298
+ */
299
+ terminal(status, mime, extra) {
300
+ return { status, mime, metadata: { extractorId: this.id, ...extra } };
301
+ }
302
+ /**
303
+ * Monta um `ExtractionResult` `failed` com diagnóstico (sem chaves `undefined`).
304
+ * @param mime - MIME real detectado.
305
+ * @param reason - Motivo canônico da falha.
306
+ * @param extra - Metadados adicionais (erro/hint/format).
307
+ */
308
+ failed(mime, reason, extra) {
309
+ return {
310
+ status: 'failed',
311
+ mime,
312
+ metadata: { extractorId: this.id, version: this.version, reason, ...extra },
313
+ };
314
+ }
315
+ }
316
+ /** Lê uma entrada de texto (UTF-8) do zip; lança se ausente (entrada esperada). */
317
+ function readText(zip, name) {
318
+ const buf = zip.read(name);
319
+ if (buf === undefined) {
320
+ throw new Error(`entrada esperada ausente no OOXML: "${name}"`);
321
+ }
322
+ return buf.toString('utf8');
323
+ }
324
+ /** Extrai a mensagem de um erro desconhecido. */
325
+ function message(error) {
326
+ return error instanceof Error ? error.message : String(error);
327
+ }
328
+ /**
329
+ * Cria um {@link OoxmlExtractor} pronto para registro. Sem trabalho assíncrono
330
+ * (não há binário a localizar) — a fábrica existe por simetria com os demais.
331
+ *
332
+ * @returns Uma instância do extrator OOXML.
333
+ */
334
+ export function createOoxmlExtractor() {
335
+ return new OoxmlExtractor();
336
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Extrator de texto de PDF via `pdftotext` (poppler) — M4-extra, #145, ADR-002.
3
+ *
4
+ * Roda o binário `pdftotext` sobre um PDF já baixado no cache e devolve a CAMADA
5
+ * DE TEXTO do documento num {@link ExtractionResult}. ESPELHA ponto-a-ponto o
6
+ * padrão do {@link TesseractExtractor} (ADR-002), todas as decisões exercitadas
7
+ * por testes:
8
+ *
9
+ * - INVOCAÇÃO SEM SHELL: usa `execFile` (nunca `exec`/shell) com lista de
10
+ * argumentos explícita (`pdftotext -enc UTF-8 <file> -`, `-` = stdout) — o
11
+ * `filePath` NUNCA é interpolado numa string de shell, eliminando injeção por
12
+ * nome de arquivo malicioso.
13
+ * - ENV SANITIZADO: o subprocesso recebe um env MÍNIMO e EXPLÍCITO (só `PATH`) —
14
+ * segredos do processo pai (ex.: `REDMINE_API_KEY`) NUNCA vazam para o pdftotext.
15
+ * - TIMEOUT + KILL: um watchdog envia `SIGTERM` no timeout e, após a graça,
16
+ * `SIGKILL` — um pdftotext travado (PDF patológico) não pendura a fila de jobs.
17
+ * - DEGRADAÇÃO GRACIOSA: binário ausente do PATH/locais convencionais NÃO lança;
18
+ * devolve `{ status: 'failed', metadata.reason }` com dica de instalação (o
19
+ * `doctor` da #53 orienta o usuário).
20
+ *
21
+ * SEMÂNTICA DO PDF SEM CAMADA DE TEXTO (escaneado): quando o `pdftotext` sai com
22
+ * sucesso mas a saída é VAZIA ou só whitespace (form-feed por página), NÃO
23
+ * devolvemos `{ status: 'done', text: '' }` — isso MENTIRIA ao consumidor,
24
+ * afirmando "extração concluída, sem texto" quando na verdade não há camada de
25
+ * texto extraível (o conteúdo é imagem, exigindo OCR). Devolvemos
26
+ * `{ status: 'failed', reason: 'pdf-sem-camada-de-texto' }` com um `hint` de que
27
+ * o OCR por página fica para uma issue futura — falha honesta e diagnosticável.
28
+ *
29
+ * A chave de cache do anexo (ADR-004) depende de `version` + `model` + `params`
30
+ * ({@link buildAttachmentKey}); por isso o extrator expõe {@link PdfExtractor.version}
31
+ * (derivada do binário real quando detectável, senão a versão da integração) e
32
+ * {@link PdfExtractor.params} (inclui `enc`/`layout`, estáveis para a chave).
33
+ */
34
+ import type { ExtractorParams } from '../cache/contract.js';
35
+ import type { ExtractorConfig } from '../cache/keys.js';
36
+ import type { ExtractionResult } from '../contract.js';
37
+ import { type ExtractOptions, type Extractor } from './dispatcher.js';
38
+ /** MIME REAL (magic bytes `%PDF`, ver `./magic.ts`) que o pdftotext aceita. */
39
+ export declare const PDF_MIMES: readonly string[];
40
+ /** Resultado de {@link findPdftotext}: caminho absoluto do binário localizado. */
41
+ export interface PdftotextLocation {
42
+ /** Caminho absoluto do executável `pdftotext` encontrado. */
43
+ readonly path: string;
44
+ }
45
+ /**
46
+ * Localiza o binário `pdftotext` no `PATH` e em locais convencionais por
47
+ * plataforma (`/opt/homebrew/bin`, `/usr/local/bin`, `C:\\Program Files\\poppler\\bin`).
48
+ * Função pura e reutilizável pelo `doctor` (#53). Não executa o binário.
49
+ *
50
+ * @returns A localização encontrada, ou `undefined` se não instalado.
51
+ * @example
52
+ * const found = findPdftotext();
53
+ * if (found === undefined) logger.warn('pdftotext (poppler) não instalado');
54
+ */
55
+ export declare function findPdftotext(): PdftotextLocation | undefined;
56
+ /** Opções de construção do {@link PdfExtractor}. */
57
+ export interface PdfExtractorOptions {
58
+ /**
59
+ * Caminho absoluto do binário já resolvido. `undefined` = não instalado; nesse
60
+ * caso {@link PdfExtractor.extract} degrada para `failed` (não lança).
61
+ */
62
+ readonly binaryPath?: string | undefined;
63
+ /** Versão exposta na chave de cache (ver {@link INTEGRATION_VERSION} como fallback). */
64
+ readonly version: string;
65
+ /**
66
+ * Preservar layout físico (`-layout`) em vez da ordem de leitura. Default `false`.
67
+ * Participa da identidade da extração (chave de cache).
68
+ */
69
+ readonly layout?: boolean;
70
+ /** Timeout antes do `SIGTERM` (ms); default {@link DEFAULT_TIMEOUT_MS}. */
71
+ readonly timeoutMs?: number;
72
+ /** Graça `SIGTERM`→`SIGKILL` (ms); default {@link DEFAULT_KILL_GRACE_MS}. */
73
+ readonly killGraceMs?: number;
74
+ }
75
+ /**
76
+ * Extrator de texto de PDF baseado no binário `pdftotext` do poppler (ADR-002).
77
+ * Implementa {@link Extractor} e, além do contrato, expõe {@link params} e
78
+ * {@link extractorConfig} estáveis para {@link buildAttachmentKey}.
79
+ */
80
+ export declare class PdfExtractor implements Extractor {
81
+ readonly id = "pdftotext";
82
+ readonly version: string;
83
+ readonly supportedMimes: readonly string[];
84
+ /** Modelo lógico para a chave de cache (ADR-004). */
85
+ readonly model = "pdftotext";
86
+ /** Parâmetros escalares estáveis (`enc`, `layout`) — participam da chave de cache. */
87
+ readonly params: ExtractorParams;
88
+ private readonly binaryPath;
89
+ private readonly layout;
90
+ private readonly timeoutMs;
91
+ private readonly killGraceMs;
92
+ /**
93
+ * @param options - Ver {@link PdfExtractorOptions}.
94
+ */
95
+ constructor(options: PdfExtractorOptions);
96
+ /**
97
+ * Configuração do extrator pronta para {@link buildAttachmentKey} (ADR-004).
98
+ * @returns `{ version, model, params }` estáveis desta instância.
99
+ */
100
+ get extractorConfig(): ExtractorConfig;
101
+ /**
102
+ * Extrai a camada de texto de `filePath`. Nunca lança: falhas (binário ausente,
103
+ * timeout, erro de execução, PDF sem texto) viram `{ status: 'failed',
104
+ * metadata.reason }` — degradação graciosa (ADR-002).
105
+ *
106
+ * IMPORTANTE (semântica): um PDF ESCANEADO (sem camada de texto) produz saída
107
+ * vazia/whitespace no `pdftotext`; nesse caso devolvemos `failed` com
108
+ * `reason: 'pdf-sem-camada-de-texto'` (e um `hint` de OCR), e NÃO
109
+ * `{ status: 'done', text: '' }` — que afirmaria falsamente sucesso sem texto.
110
+ *
111
+ * @param filePath - Caminho absoluto do PDF baixado.
112
+ * @param options - MIME real + logger — ver {@link ExtractOptions}.
113
+ * @returns `done` com `text` no sucesso; `failed` com motivo claro na falha.
114
+ */
115
+ extract(filePath: string, options: ExtractOptions): Promise<ExtractionResult>;
116
+ /**
117
+ * Monta um `ExtractionResult` `failed` com metadados de diagnóstico, sem
118
+ * injetar chaves `undefined` (respeita `exactOptionalPropertyTypes`).
119
+ *
120
+ * @param mime - MIME real detectado (do dispatcher).
121
+ * @param reason - Motivo canônico da falha.
122
+ * @param extra - Metadados adicionais (hint/erro).
123
+ * @returns Resultado `failed` tipado.
124
+ */
125
+ private failed;
126
+ }
127
+ /**
128
+ * Lê a versão do binário via `pdftotext -v`. O poppler imprime
129
+ * `pdftotext version X.Y.Z` no STDERR (não stdout) e sai com código 0. Não lança:
130
+ * retorna `undefined` se o binário falhar ou a saída for inesperada.
131
+ *
132
+ * @param bin - Caminho absoluto do binário.
133
+ * @returns A versão semântica detectada (ex.: `24.02.0`), ou `undefined`.
134
+ */
135
+ export declare function detectPdftotextVersion(bin: string): Promise<string | undefined>;
136
+ /**
137
+ * Cria um {@link PdfExtractor} resolvendo binário e versão. Localiza o
138
+ * `pdftotext` ({@link findPdftotext}); se presente, lê a versão real do binário e
139
+ * expõe `version = pdftotext-<X.Y.Z>`; se ausente (não instalado) ou versão
140
+ * ilegível, usa {@link INTEGRATION_VERSION} e o extrator degrada em `extract`.
141
+ *
142
+ * @param config - Sobrescreve `layout`/timeouts — ver {@link PdfExtractorOptions}.
143
+ * @returns O extrator pronto para registro no {@link ExtractorRegistry}.
144
+ * @example
145
+ * const extractor = await createPdfExtractor({ layout: true });
146
+ */
147
+ export declare function createPdfExtractor(config?: Omit<PdfExtractorOptions, 'version' | 'binaryPath'>): Promise<PdfExtractor>;