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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Deserto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,449 @@
1
+ # redmine-context
2
+
3
+ Consumidor de Redmine que entrega contexto completo de issues — texto e mídia (áudio/vídeo/imagem) extraída **100% localmente** — para qualquer LLM, via MCP server, CLI e TUI.
4
+
5
+ > Planejamento: `documentation/development/PLAN.md` · Backlog: `documentation/development/BACKLOG.md` · Decisões: `documentation/adr/`
6
+
7
+ ## Requisitos
8
+
9
+ - Node.js ≥ 20
10
+
11
+ ## Instalação
12
+
13
+ O pacote é publicado no npm e roda direto via `npx`, sem toolchain de compilação:
14
+
15
+ ```bash
16
+ # uso pontual (sempre a última versão), sem instalar nada global:
17
+ npx redmine-context --help
18
+ npx redmine-context issue 42 --url https://redmine.example
19
+
20
+ # ou instale o comando globalmente:
21
+ npm install -g redmine-context
22
+ redmine-context --version
23
+ ```
24
+
25
+ > Os binários de mídia (`tesseract`, `ffmpeg`, `whisper.cpp`, `pdftotext`) **não**
26
+ > são embutidos no pacote npm (ADR-002): são opcionais e instalados pelo próprio
27
+ > usuário quando quiser OCR/transcrição/PDF (ver [Binários de mídia](#binários-de-mídia-opcionais-100-local)).
28
+ > Rode `redmine-context doctor` para o diagnóstico. Sem eles, o bundle de texto é
29
+ > gerado normalmente.
30
+
31
+ ### Binários de mídia (opcionais, 100% local)
32
+
33
+ A extração de texto de anexos roda **100% na sua máquina**
34
+ ([ADR-002](documentation/adr/ADR-002-midia-100-local-politica-binarios.md)), por
35
+ binários externos que **não** acompanham o pacote npm. Todos são **opcionais**:
36
+ sem eles o bundle de texto sai normalmente (degradação graciosa) — apenas o texto
37
+ daquela mídia fica ausente, com o motivo registrado no anexo. O
38
+ [`doctor`](#troubleshooting-com-redmine-context-doctor) diz quais faltam e como
39
+ instalar no seu SO.
40
+
41
+ | Binário | Para quê | macOS (Homebrew) | Linux (apt / dnf) | Windows (winget) |
42
+ |---|---|---|---|---|
43
+ | `tesseract` | OCR de imagens (PNG/JPEG/GIF/WebP) | `brew install tesseract tesseract-lang` | `sudo apt install tesseract-ocr` · `sudo dnf install tesseract` | `winget install UB-Mannheim.TesseractOCR` |
44
+ | `pdftotext` (poppler) | Texto de anexos PDF | `brew install poppler` | `sudo apt install poppler-utils` · `sudo dnf install poppler-utils` | `winget install oschwartz10612.Poppler` (ou `choco install poppler`) |
45
+ | `ffmpeg` | Áudio/vídeo → faixa de áudio | `brew install ffmpeg` | `sudo apt install ffmpeg` · `sudo dnf install ffmpeg` | `winget install Gyan.FFmpeg` |
46
+ | `whisper.cpp` | Transcrição de áudio | `brew install whisper-cpp` | `brew install whisper-cpp` (ou compile) | baixe as [releases](https://github.com/ggml-org/whisper.cpp/releases) |
47
+
48
+ > Estes comandos são exatamente os que o `redmine-context doctor` sugere quando o
49
+ > binário está ausente. No Windows, o `tesseract` também pode ser instalado
50
+ > manualmente em `C:\Program Files\Tesseract-OCR`; o `tesseract-lang` (macOS) traz
51
+ > o traineddata `por` — o OCR usa `por+eng` por padrão.
52
+
53
+ #### Modelo do whisper.cpp (GGUF)
54
+
55
+ Além do binário, a transcrição precisa de um **modelo GGUF** (ex.: `ggml-base`) no
56
+ cache de modelos do usuário (via [`env-paths`](https://github.com/sindresorhus/env-paths),
57
+ por SO):
58
+
59
+ - **macOS**: `~/Library/Caches/redmine-context/models`
60
+ - **Linux**: `~/.cache/redmine-context/models` (respeita `$XDG_CACHE_HOME`)
61
+ - **Windows**: `%LOCALAPPDATA%\redmine-context\Cache\models`
62
+
63
+ O `doctor` reporta o status do modelo junto com os binários.
64
+
65
+ #### Download automático opt-in (`--download-binaries`) — planejado (#58)
66
+
67
+ > ⚠️ **Ainda não implementado.** O flag `--download-binaries` está **planejado**
68
+ > ([issue #58](https://github.com/vdeserto/redmine-context/issues/58)); por ora,
69
+ > instale os binários pelo gerenciador do seu SO (tabela acima) — o `doctor`
70
+ > aponta a instrução correta. Esta seção descreve o comportamento **futuro**.
71
+
72
+ Onde existe **artefato estático oficial**, o opt-in `--download-binaries` **poderá**
73
+ obter o binário/modelo automaticamente (explícito e ruidoso; **nunca** no MCP
74
+ headless): **ffmpeg** ([builds BtbN](https://github.com/BtbN/FFmpeg-Builds)),
75
+ **whisper.cpp** ([releases](https://github.com/ggml-org/whisper.cpp/releases)) e o
76
+ **modelo GGUF**. O **tesseract** não tem artefato estático oficial — instale-o
77
+ pelo gerenciador do seu SO (tabela acima). Cada download terá SHA-256 pinado, URL
78
+ fixa e escrita atômica
79
+ ([ADR-002](documentation/adr/ADR-002-midia-100-local-politica-binarios.md)).
80
+
81
+ ### Troubleshooting com `redmine-context doctor`
82
+
83
+ `redmine-context doctor` é a ferramenta central de diagnóstico do ambiente de
84
+ mídia. Ele localiza cada binário no `PATH` e em locais convencionais, lê a versão
85
+ quando disponível e, para o que estiver ausente, imprime a instrução de instalação
86
+ **do seu SO**:
87
+
88
+ ```bash
89
+ redmine-context doctor
90
+ ```
91
+
92
+ Saída (exemplo, macOS com o ffmpeg e o modelo faltando):
93
+
94
+ ```text
95
+ Binários de mídia:
96
+ [ok] tesseract v5.5.0 (/opt/homebrew/bin/tesseract)
97
+ [ok] pdftotext v24.02.0 (/opt/homebrew/bin/pdftotext)
98
+ [faltando] ffmpeg — instale com: brew install ffmpeg (ou, no futuro, o opt-in `--download-binaries`; builds estáticos BtbN (github.com/BtbN/FFmpeg-Builds))
99
+ [faltando] whisper.cpp — instale com: brew install whisper-cpp (ou, no futuro, o opt-in `--download-binaries`; releases em github.com/ggml-org/whisper.cpp/releases)
100
+ [faltando] modelo whisper (GGUF) — instale com: baixe um modelo GGUF (ex.: ggml-base) para ~/Library/Caches/redmine-context/models (ou, no futuro, o opt-in `--download-binaries`, via #58)
101
+ ```
102
+
103
+ - **Exit code**: `0` se todos os itens presentes, `1` se faltar algum — programável
104
+ em scripts/CI.
105
+ - **Ordem do relatório**: `tesseract`, `pdftotext`, `ffmpeg`, `whisper.cpp`,
106
+ `modelo whisper (GGUF)`.
107
+ - Degrada em `NO_COLOR`/saída não-TTY (texto puro, sem ANSI).
108
+
109
+ ### Keychain do sistema: prebuilds e fallback
110
+
111
+ O keychain nativo (`@napi-rs/keyring`) é distribuído como **binários pré-compilados
112
+ por plataforma**, publicados como `optionalDependencies`. Na instalação, o npm baixa
113
+ apenas o prebuild da sua plataforma — **`node-gyp` nunca é acionado**, então não há
114
+ toolchain de compilação (C/C++/Python) envolvida. As plataformas com prebuild
115
+ declarado são `darwin-arm64`, `darwin-x64`, `linux-x64-gnu` e `win32-x64-msvc`.
116
+
117
+ Em uma plataforma **sem prebuild** (por exemplo, Linux **musl**/Alpine, ou uma
118
+ arquitetura exótica), o pacote opcional correspondente simplesmente **falha em
119
+ silêncio** e o npm o ignora — a instalação continua verde, **sem erro e sem
120
+ compilar nada**. Em tempo de execução, o módulo de credenciais detecta a ausência
121
+ do binário (o import dinâmico falha de forma controlada), emite **um único aviso**
122
+ e **degrada para o arquivo `0600`** da cascata do ADR-003 (keychain → arquivo →
123
+ `REDMINE_API_KEY`). O login e o boot nunca são interrompidos por falta de keychain.
124
+
125
+ ## Quickstart
126
+
127
+ Do login ao contexto da issue no seu LLM, em três passos:
128
+
129
+ ```bash
130
+ # 1) login — autentica e grava a api_key da instância na cascata de credenciais:
131
+ # keychain do sistema (preferido) → arquivo 0600 → REDMINE_API_KEY (env).
132
+ # Credenciais antigas em arquivo migram para o keychain automaticamente.
133
+ # (senha ou, em contas com 2FA, cole a api_key quando solicitado).
134
+ # Também SALVA a instância como default: depois do login, o --url/REDMINE_URL
135
+ # passam a ser opcionais na CLI e na TUI (precedência: --url → REDMINE_URL → salva).
136
+ redmine-context login --url https://redmine.example
137
+
138
+ # 2) issue — imprime o bundle Markdown completo da issue em stdout
139
+ # (descrição + histórico + custom fields + anexos + relações + pai/filhos).
140
+ redmine-context issue 42 # usa a instância salva no login (ou --url/REDMINE_URL)
141
+ # --json grava/emite o bundle JSON canônico; --out <dir> grava em arquivo.
142
+ # --extract liga o OCR dos anexos de imagem e embute o texto no bundle
143
+ # (requer tesseract; ver Extração de mídia abaixo).
144
+
145
+ # 3) mcp add — registra o MCP server no seu cliente (ex.: Claude) para expor as
146
+ # tools read-only get_issue_context, search_issues e get_attachment_text,
147
+ # usando a mesma credencial da cascata.
148
+ claude mcp add redmine-context \
149
+ --env REDMINE_URL=https://redmine.example \
150
+ -- npx -y redmine-context mcp
151
+ ```
152
+
153
+ Para o ambiente Docker local (http), passe `--insecure` na CLI e
154
+ `REDMINE_INSECURE=1` no ambiente do MCP server (TLS é obrigatório por padrão;
155
+ ver [Ambiente de teste](#ambiente-de-teste) e [E2E](#e2e-dogfood-cli--mcp)).
156
+
157
+ ## Scripts
158
+
159
+ | Script | O que faz |
160
+ |---|---|
161
+ | `npm run typecheck` | Checagem de tipos (tsc, strict) |
162
+ | `npm run lint` | ESLint (typescript-eslint) |
163
+ | `npm test` | Vitest com cobertura (threshold 80%) |
164
+ | `npm run build` | Compila para `dist/` |
165
+ | `npm run seed` | Popula fixtures base no Redmine via REST (ver [Seed](#seed-de-fixtures)) |
166
+ | `npm run e2e` | Roteiro E2E de dogfood (CLI + MCP) contra o Docker (ver [E2E](#e2e-dogfood-cli--mcp)) |
167
+ | `npm run ci:e2e:up` | Sobe + espera healthy/one-shots + seed em 1 comando, p/ o CI (ver [CI](#subir-e-seedar-em-1-comando-ci)) |
168
+ | `npm run record:fixtures` | Grava fixtures HTTP p/ o replay offline (ver [Replay offline](#replay-offline-via-fixtures-gravadas-macoswindows)) |
169
+ | `npm run changeset` | Cria um changeset (bump + nota de CHANGELOG) — ver [Release](#release-changesets) |
170
+ | `npm run version` | `changeset version` — aplica bumps e escreve o `CHANGELOG.md` |
171
+ | `npm run release` | `changeset publish` — publica no npm (usado só pela workflow de release) |
172
+
173
+ ## Release (Changesets)
174
+
175
+ Versionamento e publicação npm via [`@changesets/cli`](https://github.com/changesets/changesets).
176
+
177
+ - **Crie um changeset em todo PR de código:** `npm run changeset` (escolha `patch`/`minor`/`major` + a nota). Commite o `.changeset/<slug>.md` junto.
178
+ - **Gate de CI:** `changeset-check.yml` roda `changeset status --since=origin/main` no PR e **reprova** código sem changeset (docs/CI ficam isentos).
179
+ - **Release automático:** `release.yml` (push em `main`, após gates verdes) usa `changesets/action` para abrir o PR **"Version Packages"** (bump + CHANGELOG) e, ao mergeá-lo, roda `npm publish` com **provenance** (`NPM_CONFIG_PROVENANCE` + OIDC `id-token: write`).
180
+ - **Guardado por `NPM_TOKEN`:** o publish só roda com o segredo presente — sem ele o workflow fica inerte (nada é publicado).
181
+ - **Dry-run (não publica):** `npm publish --dry-run` simula o empacotamento do `dist/`.
182
+
183
+ Detalhes e passos manuais (adicionar `NPM_TOKEN`, tornar o repo público, disparar o 1º release): `documentation/development/PLAN.md`.
184
+
185
+ ## Estrutura
186
+
187
+ `src/` segue os 6 módulos do core (ADR-005): `client` (REST Redmine), `normalize`, `extract` (pipeline de mídia), `bundle`, `config` (auth/credenciais — cascata keychain → arquivo → env), `cache`. Superfícies: CLI e MCP (M1) e TUI interativa (M2) em `src/surfaces/`.
188
+
189
+ ## MCP server (stdio)
190
+
191
+ O subcomando `redmine-context mcp` sobe um servidor [MCP](https://modelcontextprotocol.io) sobre stdio, expondo três tools read-only:
192
+
193
+ - `get_issue_context(issue_id: number, format?: 'markdown' | 'json', extract_attachments?: boolean)` — busca a issue na instância configurada, normaliza e retorna o bundle (Markdown por padrão). Com `extract_attachments: true`, embute o texto (OCR) dos anexos de imagem no bundle (default `false`, pois adiciona latência de download+OCR).
194
+ - `search_issues(query?, project_id?, status_id?, assigned_to_id?, updated_on?, limit?)` — busca issues por filtros estruturados e, opcionalmente, texto livre (`query`, best-effort via `/search`); retorna uma lista compacta paginada.
195
+ - `get_attachment_text(issue_id: number, attachment_id: number)` — retorna o texto extraído (OCR, com cache) de um anexo, dentro de uma fence de conteúdo não confiável. Anexo não processável retorna o status/motivo legível (`skipped`/`unsupported`/`failed`), nunca um erro genérico.
196
+
197
+ A instância vem sempre da configuração do processo (`REDMINE_URL` + cascata de credencial, `REDMINE_API_KEY` no modo headless): **nenhuma tool aceita URL/host arbitrário**. Erros 403/404 e credencial ausente retornam um erro MCP claro (`isError`). O stdout é reservado ao protocolo; logs vão para stderr.
198
+
199
+ Registro no Claude (tudo após `--` é o comando do servidor stdio; `npx -y` roda a
200
+ última versão publicada, sem instalar nada global):
201
+
202
+ ```bash
203
+ claude mcp add redmine-context -- npx -y redmine-context mcp
204
+ ```
205
+
206
+ No Windows nativo (fora do WSL), o `npx` precisa do wrapper `cmd /c`:
207
+
208
+ ```bash
209
+ claude mcp add redmine-context -- cmd /c npx -y redmine-context mcp
210
+ ```
211
+
212
+ Passe a instância via `--env` (aplicado ao ambiente do servidor), por exemplo
213
+ `claude mcp add redmine-context --env REDMINE_URL=https://redmine.example -- npx -y redmine-context mcp`.
214
+ Configure o ambiente do servidor com `REDMINE_URL` e `REDMINE_API_KEY` (ou rode
215
+ `redmine-context login` para gravar a credencial na cascata). Com a integração
216
+ ativa, o cliente ganha as tools read-only `get_issue_context`, `search_issues` e
217
+ `get_attachment_text` (detalhadas acima).
218
+
219
+ ## Extração de mídia (OCR)
220
+
221
+ O texto de anexos de **imagem** (PNG/JPEG/GIF/WebP) é extraído **100% localmente**
222
+ via [`tesseract`](https://github.com/tesseract-ocr/tesseract) (idiomas `por+eng`
223
+ por padrão), conforme o [ADR-002](documentation/adr/ADR-002-midia-100-local-politica-binarios.md).
224
+ O texto extraído é sempre marcado como **não confiável** (`<untrusted-content>`)
225
+ no bundle. Áudio/vídeo entram no M4 pelo mesmo caminho.
226
+
227
+ Documentos **Office (OOXML)** — `.docx`, `.pptx`, `.xlsx` — também têm o texto
228
+ extraído **100% localmente e SEM binário externo** (são contêineres ZIP com XML;
229
+ o texto é lido direto no Node). Funciona nos 3 SOs sem instalar nada e **não** entra
230
+ no `doctor`. Cobertura: parágrafos do documento (docx), texto dos slides na ordem
231
+ (pptx) e as *shared strings* (xlsx); formatação complexa/tabelas saem simplificadas.
232
+ Um `.doc`/`.ppt`/`.xls` antigo (formato binário pré-2007) não é OOXML e não é suportado.
233
+
234
+ - **CLI**: `redmine-context issue <id> --extract` baixa os anexos, roda o OCR e
235
+ embute o texto no bundle.
236
+ - **MCP**: `get_issue_context(..., extract_attachments: true)` e
237
+ `get_attachment_text(issue_id, attachment_id)`.
238
+
239
+ **Degradação graciosa**: o `tesseract` **não** é pré-requisito. Se estiver
240
+ ausente, o bundle é gerado do mesmo jeito — o anexo apenas registra o status de
241
+ falha com a dica de instalação; a falha de um anexo nunca derruba os demais nem o
242
+ bundle. As extrações são cacheadas por `(instância, anexo, digest, versão+modelo+params
243
+ do extrator)` ([ADR-004](documentation/adr/ADR-004-cache-duas-camadas.md)): um
244
+ comentário novo **não** reprocessa o OCR, e CLI e MCP compartilham o mesmo cache.
245
+
246
+ Verifique a instalação de todos os binários (tesseract, pdftotext, ffmpeg,
247
+ whisper.cpp) e do modelo GGUF com o `doctor` — ver
248
+ [Troubleshooting com `redmine-context doctor`](#troubleshooting-com-redmine-context-doctor):
249
+
250
+ ```bash
251
+ redmine-context doctor # exit 0 se tudo presente, 1 se faltar algum
252
+ ```
253
+
254
+
255
+ ## TUI interativa
256
+
257
+ `redmine-context` **sem argumentos** (num terminal interativo) abre a interface
258
+ de texto completa — mesma credencial e mesmo core da CLI/MCP:
259
+
260
+ | Tela | Como chegar | Para quê |
261
+ |---|---|---|
262
+ | Início | abertura | roteia para onboarding (sem credencial) ou Home |
263
+ | Onboarding | `Enter` no Início | URL → modo de auth → login (senha mascarada) → splash |
264
+ | Home | pós-login | suas issues com estados, seleção e retry |
265
+ | Busca | `/` na Home | full-text + filtro de status (`f` com a busca fechada) |
266
+ | Detalhe | `Enter` numa issue | metadados, descrição/journals roláveis, anexos |
267
+ | Exportação | `e` no Detalhe | grava o bundle MD/JSON (destino com `~`) |
268
+ | Jobs | `t` | operações da sessão |
269
+ | Doctor / Config | `d` / `c` no Início | status do ambiente · logout |
270
+ | Aparência | `a` no Início | escolher a **paleta de cores** (preview ao vivo) |
271
+
272
+ Atalhos globais: `Esc` volta · `q` sai · `Ctrl+C` duas vezes sai · `?` atalhos.
273
+ Sessão expirada (401) reabre o login e retoma a operação automaticamente.
274
+ Em `NO_COLOR`, `CI=true` ou saída não-TTY, a TUI cede lugar ao modo texto puro.
275
+
276
+ ### Full-screen e paletas de cores
277
+
278
+ Em um terminal com cor, a TUI abre em **tela cheia** (alt-screen, como o vim/htop) —
279
+ o conteúdo anterior do terminal é restaurado ao sair. As cores vêm de **kits de
280
+ paletas** consagradas em truecolor: **Catppuccin Mocha** (default), **Dracula**,
281
+ **Nord**, **Tokyo Night**, **Gruvbox Dark**, **Rosé Pine**, **Solarized Dark** e
282
+ **One Dark**. Abra a tela **Aparência** (`a` no Início), navegue com `↑`/`↓` para
283
+ pré-visualizar ao vivo, `Enter` salva (persistida em `settings.json`, por SO via
284
+ `env-paths`) e `Esc` cancela. A escolha vale para os próximos boots.
285
+
286
+ ## Ambiente de teste
287
+
288
+ Um ambiente Redmine + Postgres descartável, usado pelos testes locais e
289
+ reutilizado sem fork pelo CI (M5). Definição em [`docker/`](docker/).
290
+
291
+ - `docker/docker-compose.yml` — Redmine (`redmine:6`) + Postgres
292
+ (`postgres:16-alpine`), volumes nomeados (`pgdata`, `redmine_files`) e
293
+ healthchecks (`pg_isready` no Postgres; `wget` na raiz do Redmine).
294
+ - `docker/wait-for-healthy.sh` — bloqueia até os serviços ficarem `healthy`.
295
+ - `docker/.env.example` — variáveis parametrizáveis (copie para `docker/.env`).
296
+
297
+ ### Subir
298
+
299
+ ```bash
300
+ docker compose -f docker/docker-compose.yml up -d
301
+ docker/wait-for-healthy.sh # aguarda postgres + redmine healthy
302
+ ```
303
+
304
+ A interface web e a **REST API** ficam em `http://localhost:${REDMINE_PORT}`
305
+ (porta padrão **3080**, fixa e documentada). A REST API é habilitada
306
+ automaticamente após o `up`, sem passo manual: um serviço one-shot
307
+ (`enable-rest-api`) roda assim que o Redmine fica healthy e liga
308
+ `rest_api_enabled` via SQL (idempotente). O seed do M1-02.2 reutiliza a API
309
+ com uma chave.
310
+
311
+ ### Verificar (smoke test)
312
+
313
+ ```bash
314
+ # API servindo (login não é exigido por padrão, lista vazia):
315
+ curl -s http://localhost:3080/issues.json # {"issues":[],...} (200)
316
+
317
+ # Endpoint protegido rejeita sem chave => API de pé e autenticando:
318
+ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3080/users.json # 401
319
+ ```
320
+
321
+ ### Seed de fixtures
322
+
323
+ Com o ambiente de pé (healthy + REST habilitada), popule fixtures base:
324
+
325
+ ```bash
326
+ npm run seed
327
+ ```
328
+
329
+ O script (`scripts/seed.mjs`, Node puro, `fetch` nativo, sem dependências) cria
330
+ um projeto de identifier fixo **`rc-fixtures`** e **3 issues** com descrição, via
331
+ REST API. É **idempotente**: cada recurso é verificado por `GET` antes do `POST`
332
+ (projeto por identifier, issues por subject), então re-executar não duplica nada.
333
+ Ao final, asserções via `GET` validam as contagens e o processo sai com código
334
+ `!= 0` se algo estiver errado.
335
+
336
+ Config via ambiente (defaults combinam com o compose de teste):
337
+
338
+ | Variável | Padrão | Descrição |
339
+ | ------------------------ | ------------------------ | ---------------------------- |
340
+ | `REDMINE_URL` | `http://localhost:3080` | Base da REST API |
341
+ | `REDMINE_ADMIN_USER` | `admin` | Usuário admin (Basic auth) |
342
+ | `REDMINE_ADMIN_PASSWORD` | `admin` | Senha do admin (Basic auth) |
343
+
344
+ > Senha do admin: a imagem oficial do Redmine 6 marca o admin default para
345
+ > trocar a senha no primeiro login **web** (`must_change_passwd`). Esse flag é
346
+ > por sessão e **não bloqueia a REST API via Basic auth**; ainda assim o seed o
347
+ > zera de forma proativa e idempotente (`PUT /users/{id}.json`, mantendo a mesma
348
+ > senha) para manter o admin utilizável em web + API.
349
+
350
+ ### Subir e seedar em 1 comando (CI)
351
+
352
+ Para o job de E2E no CI (Linux, issue #80), o stack precisa ficar **pronto e
353
+ seedado** em um único passo. `scripts/ci-e2e-up.sh` faz exatamente isso,
354
+ **reutilizando** o compose, o `wait-for-healthy.sh` e o `seed.mjs` (sem fork):
355
+
356
+ ```bash
357
+ npm run ci:e2e:up # = bash scripts/ci-e2e-up.sh
358
+ ```
359
+
360
+ Passos: `docker compose up -d` → `wait-for-healthy.sh postgres redmine` → poll de
361
+ `docker compose ps -a` até os one-shots (`load-default-data`, `enable-rest-api`)
362
+ saírem com código 0 → `npm run seed` (idempotente). O poll de `ps -a` é usado no
363
+ lugar de `docker compose wait` porque este erra quando o container já saiu (só
364
+ rastreia containers em execução). Em sucesso o stack fica **de pé** (o teardown,
365
+ `down -v`, é responsabilidade do chamador). Re-executar é seguro: o seed não
366
+ duplica. Aceita `COMPOSE_FILE`/`ONESHOT_TIMEOUT`/`TIMEOUT` e as variáveis do seed.
367
+
368
+ ### E2E dogfood (CLI + MCP)
369
+
370
+ Com Docker disponível, o roteiro E2E automatizado valida o fluxo real
371
+ ponta-a-ponta (issue #20 / M1-14):
372
+
373
+ ```bash
374
+ npm run e2e
375
+ ```
376
+
377
+ O script (`scripts/e2e.mjs`, Node puro, sem dependências, log em stderr, exit
378
+ `!= 0` em falha) sobe o stack (ou assume up com `E2E_ASSUME_UP=1`), espera
379
+ healthy + one-shots, roda o seed, builda, exercita a **CLI** para as 3 issues
380
+ do seed (grep das strings das fixtures: descrição, journal, custom field, anexo,
381
+ relação e pai/filho), sobe o **MCP server** como subprocess e chama
382
+ `get_issue_context` via um cliente JSON-RPC stdio mínimo (initialize →
383
+ initialized → tools/call), compara **MCP vs CLI** byte-a-byte, mede o tempo
384
+ issue→bundle (cache quente, orçamento < 30 s), testa a **recusa de http:// sem
385
+ `--insecure`** (exit `!= 0`) e faz o teardown (`down -v`) ao final.
386
+
387
+ > A api_key é obtida no fluxo real (`GET /users/current.json` com Basic auth
388
+ > admin). Contra o Docker local (http) a CLI usa `--insecure` e o MCP server
389
+ > lê `REDMINE_INSECURE=1` do ambiente.
390
+
391
+ ### Replay OFFLINE via fixtures gravadas (macOS/Windows)
392
+
393
+ O E2E real acima depende de Docker e só roda no Linux do CI (#80). Para dar
394
+ cobertura E2E-equivalente **offline** na matriz **macOS/Windows** (#77) — onde o
395
+ Redmine não está disponível —, gravamos as respostas HTTP do Redmine seedado como
396
+ **fixtures versionadas** e as **reproduzimos sem rede nem docker** (issue #81):
397
+
398
+ - **Fixtures**: `tests/fixtures/redmine-e2e/interactions.json` — as interações
399
+ `GET /issues/{id}.json` (com `include`) das 3 issues do seed e o download do
400
+ anexo de texto. **Segredos redigidos**: nenhuma `api_key`, `Authorization` ou
401
+ senha real; a URL base do stack é reescrita para `https://redmine.example`.
402
+ - **Replay**: `tests/integration/redmine-replay.test.ts` — substitui o `fetch`
403
+ global por um stub dirigido pelas fixtures (mesmo mecanismo dos demais testes
404
+ do core; o client usa `fetch`/undici, que o `nock` não intercepta) e reexecuta
405
+ `getIssue → normalize → bundle` + o download do anexo, comparando contra
406
+ snapshots determinísticos. Requisições **não gravadas lançam** — se algo tentar
407
+ a rede, o teste falha. Roda na suíte padrão (`npm test`), portanto na matriz de
408
+ 3 SOs do CI.
409
+
410
+ **Regravar as fixtures** (só quando o seed/contrato mudar):
411
+
412
+ ```bash
413
+ npm run ci:e2e:up # sobe + espera healthy/one-shots + seed (precisa de Docker)
414
+ npm run record:fixtures # grava tests/fixtures/redmine-e2e/interactions.json (segredos redigidos)
415
+ npx vitest run tests/integration/redmine-replay.test.ts -u # atualiza os snapshots
416
+ git add tests/fixtures/redmine-e2e tests/integration/__snapshots__
417
+ ```
418
+
419
+ O `scripts/record-fixtures.mjs` obtém a `api_key` do admin no fluxo real
420
+ (`GET /users/current.json`, Basic auth), mas **nunca** a grava: ela viaja só no
421
+ header (não serializado), campos sensíveis são redigidos para `[REDACTED]` e o
422
+ script **aborta** se a chave real aparecer no arquivo. O teste versionado
423
+ reafirma a ausência de segredos por grep.
424
+
425
+ ### Reproduzir instância limpa / derrubar
426
+
427
+ ```bash
428
+ docker compose -f docker/docker-compose.yml down -v # remove containers + volumes
429
+ docker compose -f docker/docker-compose.yml up -d # instância nova do zero
430
+ ```
431
+
432
+ ### Parametrização
433
+
434
+ Definidas em `docker/.env` (auto-carregado) ou como variáveis de ambiente:
435
+
436
+ | Variável | Padrão | Descrição |
437
+ | ------------------------- | ------------------------------- | ----------------------------- |
438
+ | `REDMINE_PORT` | `3080` | Porta host da web/REST API |
439
+ | `POSTGRES_DB` | `redmine` | Nome do banco |
440
+ | `POSTGRES_USER` | `redmine` | Usuário do banco |
441
+ | `POSTGRES_PASSWORD` | `redmine` | Senha do banco (test-only) |
442
+ | `REDMINE_SECRET_KEY_BASE` | `please-change-me-in-real-envs` | Secret do Rails |
443
+
444
+ O script `wait-for-healthy.sh` aceita `TIMEOUT`, `INTERVAL` e `COMPOSE_FILE`.
445
+
446
+ > As credenciais acima são exclusivas do ambiente de teste (defaults no
447
+ > compose/`.env.example`); não há credenciais hardcoded fora dele.
448
+
449
+ > Nota (CI): o one-shot `enable-rest-api` roda após o Redmine ficar healthy; em pipelines, aguarde o exit 0 dele antes do smoke test da API — o `ci-e2e-up.sh` faz isso via poll de `docker compose ps -a` (não `docker compose wait`, que erra em containers já finalizados).
@@ -0,0 +1,5 @@
1
+ export declare const MODULE_NAME: "bundle";
2
+ export { buildJsonBundle, type JsonBundle, type JsonBundleEnvelope, type JsonBundleMeta, type JsonBundleSource, } from './json.js';
3
+ export { buildMarkdownBundle, fenceBlock, fenceInline, type MarkdownBundleMeta } from './markdown.js';
4
+ export { buildSearchListMarkdown, type SearchListItem, type SearchListMeta, } from './search-list.js';
5
+ export { stableStringify, type JsonValue } from './stable-stringify.js';
@@ -0,0 +1,5 @@
1
+ export const MODULE_NAME = 'bundle';
2
+ export { buildJsonBundle, } from './json.js';
3
+ export { buildMarkdownBundle, fenceBlock, fenceInline } from './markdown.js';
4
+ export { buildSearchListMarkdown, } from './search-list.js';
5
+ export { stableStringify } from './stable-stringify.js';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Bundle JSON determinístico de uma issue normalizada (issue #15, ADR-005).
3
+ *
4
+ * Converte uma {@link Issue} do contrato num corpo canônico byte-idêntico entre
5
+ * execuções do mesmo estado, próprio para consumo por LLMs. Três invariantes:
6
+ *
7
+ * 1. Determinismo: coleções recebem ordenação estável DECLARADA (journals por
8
+ * `(created_on, id)`; attachments/relations/custom_fields/children por `id`) e
9
+ * as chaves de objeto são ordenadas pelo serializador ({@link stableStringify}).
10
+ * 2. `generated_at` FORA do corpo canônico: vive no envelope separado, para que
11
+ * dois bundles do mesmo estado sejam idênticos. O chamador (CLI, #17) decide
12
+ * como unir corpo e envelope.
13
+ * 3. Anti prompt-injection: conteúdo textual derivado do Redmine (descrição, notas
14
+ * de journal, valores de custom field) é marcado `untrusted: true` (ADR-005).
15
+ *
16
+ * Follow-up da review da #11: refs degradadas — o `NULL_REF` congelado do
17
+ * normalize, com `id === 0` — NÃO são renderizadas como valores válidos; emitem
18
+ * `null` explícito, evitando que um placeholder vaze como referência real.
19
+ */
20
+ import type { ExtractionResult, Issue, Journal } from '../contract.js';
21
+ /** Mapa `attachmentId → resultado da extração` produzido pelo pipeline (M3-10). */
22
+ export type ExtractionMap = ReadonlyMap<number, ExtractionResult>;
23
+ /** Metadados do empacotamento fornecidos pela superfície chamadora. */
24
+ export interface JsonBundleMeta {
25
+ /** Base URL do Redmine de origem (compõe o envelope `source`). */
26
+ baseUrl: string;
27
+ /** Versão da ferramenta (`TOOL_VERSION`), gravada no corpo canônico. */
28
+ toolVersion: string;
29
+ /**
30
+ * Timestamp ISO do empacotamento. Injetável para testes/reprodutibilidade;
31
+ * quando omitido, usa o relógio do sistema. NUNCA entra no corpo canônico.
32
+ */
33
+ generatedAt?: string;
34
+ /**
35
+ * Extrações de anexos (M3-10). Quando presente, cada anexo com resultado ganha
36
+ * `extraction` no corpo; o texto de OCR entra como conteúdo `untrusted`.
37
+ */
38
+ extractions?: ExtractionMap;
39
+ }
40
+ /** Proveniência da issue empacotada. */
41
+ export interface JsonBundleSource {
42
+ base_url: string;
43
+ issue_id: number;
44
+ issue_updated_on: string;
45
+ }
46
+ /** Envelope não-canônico: carrega o que varia entre execuções (`generated_at`). */
47
+ export interface JsonBundleEnvelope {
48
+ generated_at: string;
49
+ source: JsonBundleSource;
50
+ }
51
+ /** Resultado do empacotamento: corpo canônico (string) + envelope (objeto). */
52
+ export interface JsonBundle {
53
+ /** Corpo canônico serializado — determinístico e byte-idêntico por estado. */
54
+ canonical: string;
55
+ /** Envelope separado; o chamador decide como unir ao corpo. */
56
+ envelope: JsonBundleEnvelope;
57
+ }
58
+ /**
59
+ * Comparador estável de journals: `created_on` e, em empate, `id`.
60
+ *
61
+ * Exportado para que o bundle Markdown (#16) reutilize EXATAMENTE a mesma
62
+ * ordenação cronológica do JSON, sem duplicar a regra de desempate.
63
+ */
64
+ export declare function compareJournals(a: Journal, b: Journal): number;
65
+ /**
66
+ * Ordena por `id` sem mutar o array de entrada.
67
+ *
68
+ * Exportado para reuso pelo bundle Markdown (#16): attachments, relations,
69
+ * custom_fields e children compartilham a MESMA ordenação estável do JSON.
70
+ */
71
+ export declare function byId<T extends {
72
+ id: number;
73
+ }>(items: readonly T[]): T[];
74
+ /**
75
+ * Empacota uma issue normalizada num bundle JSON determinístico.
76
+ *
77
+ * O corpo canônico é byte-idêntico entre execuções do mesmo estado (ordenação
78
+ * estável + serializador de chaves ordenadas). `generated_at` fica no envelope,
79
+ * fora do corpo, preservando essa propriedade.
80
+ *
81
+ * @param issue - Issue normalizada (ver `src/normalize`).
82
+ * @param meta - Metadados do empacotamento (base URL, versão, timestamp opcional).
83
+ * @returns Corpo canônico serializado e envelope com `generated_at` + `source`.
84
+ * @example
85
+ * const { canonical, envelope } = buildJsonBundle(issue, {
86
+ * baseUrl: 'https://redmine.example',
87
+ * toolVersion: TOOL_VERSION,
88
+ * });
89
+ */
90
+ export declare function buildJsonBundle(issue: Issue, meta: JsonBundleMeta): JsonBundle;